240 lines
9.5 KiB
Go
240 lines
9.5 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"mime/multipart"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||
)
|
||
|
||
type certificateReaderStub struct {
|
||
result certificate.IssueResult
|
||
versions []certificate.SavedResource
|
||
err error
|
||
}
|
||
|
||
func (s certificateReaderStub) Current(context.Context, string) (certificate.IssueResult, error) {
|
||
return s.result, s.err
|
||
}
|
||
|
||
func (s certificateReaderStub) ListVersions(context.Context, string) ([]certificate.SavedResource, error) {
|
||
return s.versions, s.err
|
||
}
|
||
|
||
func TestCertificateReadAPIs(t *testing.T) {
|
||
expiresAt := time.Now().UTC().Add(90 * 24 * time.Hour)
|
||
reader := certificateReaderStub{
|
||
result: certificate.IssueResult{
|
||
DomainID: "domain-1",
|
||
Domains: []string{"example.com", "www.example.com"},
|
||
Saved: certificate.SavedResource{
|
||
Domain: "example.com",
|
||
CertificateType: "san",
|
||
Version: "20260827-120000.000000000",
|
||
NotAfter: expiresAt,
|
||
},
|
||
},
|
||
versions: []certificate.SavedResource{{Version: "20260827-120000.000000000"}},
|
||
}
|
||
repo := &stubDomainRepository{items: []domain.Domain{{ID: "domain-1", Name: "example.com", CertificateType: domain.CertificateSAN}}}
|
||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: repo, CurrentCertificate: reader})
|
||
|
||
list := httptest.NewRecorder()
|
||
serveAdminHTTP(server, list, httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil))
|
||
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), "example.com") {
|
||
t.Fatalf("证书列表响应错误:%d %s", list.Code, list.Body.String())
|
||
}
|
||
|
||
detail := httptest.NewRecorder()
|
||
serveAdminHTTP(server, detail, httptest.NewRequest(http.MethodGet, "/api/v1/certificates/domain-1", nil))
|
||
if detail.Code != http.StatusOK || !strings.Contains(detail.Body.String(), "20260827-120000.000000000") {
|
||
t.Fatalf("证书详情响应错误:%d %s", detail.Code, detail.Body.String())
|
||
}
|
||
|
||
versions := httptest.NewRecorder()
|
||
serveAdminHTTP(server, versions, httptest.NewRequest(http.MethodGet, "/api/v1/domains/domain-1/certificate/versions", nil))
|
||
if versions.Code != http.StatusOK || !strings.Contains(versions.Body.String(), "20260827-120000.000000000") {
|
||
t.Fatalf("证书历史版本响应错误:%d %s", versions.Code, versions.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestCertificateListHidesReaderErrors(t *testing.T) {
|
||
reader := certificateReaderStub{err: errors.New("private-key-secret-path")}
|
||
repo := &stubDomainRepository{
|
||
items: []domain.Domain{{ID: "domain-1", Name: "example.com"}},
|
||
}
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: repo,
|
||
CurrentCertificate: reader,
|
||
})
|
||
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(
|
||
server,
|
||
response,
|
||
httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil),
|
||
)
|
||
|
||
if response.Code != http.StatusOK {
|
||
t.Fatalf("certificate list request failed: %d %s", response.Code, response.Body.String())
|
||
}
|
||
|
||
body := response.Body.String()
|
||
if strings.Contains(body, "private-key-secret-path") {
|
||
t.Fatalf("certificate list leaked an internal error: %s", body)
|
||
}
|
||
if !strings.Contains(body, "\u5f53\u524d\u8bc1\u4e66\u4e0d\u53ef\u7528") {
|
||
t.Fatalf("certificate list did not include the safe error message: %s", body)
|
||
}
|
||
}
|
||
func TestCertificateDeployAPIHidesWorkflowErrors(t *testing.T) {
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: &stubDomainRepository{},
|
||
DeployCertificate: func(context.Context, string) (certificate.IssueResult, deployment.Report, error) {
|
||
return certificate.IssueResult{}, deployment.Report{}, errors.New("private-key-secret-path")
|
||
},
|
||
})
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/deploy", nil))
|
||
if response.Code != http.StatusBadGateway {
|
||
t.Fatalf("certificate deployment failure returned an unexpected status: %d %s", response.Code, response.Body.String())
|
||
}
|
||
body := response.Body.String()
|
||
if strings.Contains(body, "private-key-secret-path") {
|
||
t.Fatalf("certificate deployment leaked an internal error: %s", body)
|
||
}
|
||
if !strings.Contains(body, "\u8bc1\u4e66\u90e8\u7f72\u5931\u8d25") {
|
||
t.Fatalf("certificate deployment did not include the safe error message: %s", body)
|
||
}
|
||
}
|
||
|
||
func TestCertificateImportAPIHidesWorkflowErrors(t *testing.T) {
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: &stubDomainRepository{},
|
||
ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||
return certificate.IssueResult{}, deployment.Report{}, errors.New("private-key-secret-path")
|
||
},
|
||
})
|
||
body := `{"certificate_path":"/ssl/cert.pem","private_key_path":"/ssl/key.pem"}`
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(server, response, request)
|
||
if response.Code != http.StatusBadGateway {
|
||
t.Fatalf("certificate import failure returned an unexpected status: %d %s", response.Code, response.Body.String())
|
||
}
|
||
responseBody := response.Body.String()
|
||
if strings.Contains(responseBody, "private-key-secret-path") {
|
||
t.Fatalf("certificate import leaked an internal error: %s", responseBody)
|
||
}
|
||
if !strings.Contains(responseBody, "\u8bc1\u4e66\u5bfc\u5165\u5931\u8d25") {
|
||
t.Fatalf("certificate import did not include the safe error message: %s", responseBody)
|
||
}
|
||
}
|
||
func TestCertificateImportAPIAcceptsMultipartFiles(t *testing.T) {
|
||
var received certificate.ImportRequest
|
||
importer := func(_ context.Context, request certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||
received = request
|
||
certificateData, err := os.ReadFile(request.Certificate)
|
||
if err != nil {
|
||
return certificate.IssueResult{}, deployment.Report{}, err
|
||
}
|
||
privateKeyData, err := os.ReadFile(request.PrivateKey)
|
||
if err != nil {
|
||
return certificate.IssueResult{}, deployment.Report{}, err
|
||
}
|
||
if string(certificateData) != "certificate" || string(privateKeyData) != "private-key" {
|
||
return certificate.IssueResult{}, deployment.Report{}, os.ErrInvalid
|
||
}
|
||
return certificate.IssueResult{DomainID: request.DomainID}, deployment.Report{Status: "success"}, nil
|
||
}
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: &stubDomainRepository{},
|
||
ImportCertificate: importer,
|
||
})
|
||
|
||
body := &bytes.Buffer{}
|
||
writer := multipart.NewWriter(body)
|
||
certificatePart, err := writer.CreateFormFile("certificate", "cert.pem")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, _ = certificatePart.Write([]byte("certificate"))
|
||
privateKeyPart, err := writer.CreateFormFile("private_key", "key.pem")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, _ = privateKeyPart.Write([]byte("private-key"))
|
||
if err := writer.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", body)
|
||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(server, response, request)
|
||
if response.Code != http.StatusOK || received.DomainID != "domain-1" {
|
||
t.Fatalf("证书导入响应错误:%d %s", response.Code, response.Body.String())
|
||
}
|
||
if _, err := os.Stat(received.Certificate); !os.IsNotExist(err) {
|
||
t.Fatalf("证书上传临时文件未在请求结束后清理:%s", received.Certificate)
|
||
}
|
||
}
|
||
|
||
func TestCertificateImportAPIRejectsMissingFiles(t *testing.T) {
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: &stubDomainRepository{},
|
||
ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||
return certificate.IssueResult{}, deployment.Report{}, nil
|
||
},
|
||
})
|
||
body, _ := json.Marshal(certificatePathRequest{CertificatePath: "", PrivateKeyPath: ""})
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", bytes.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(server, response, request)
|
||
if response.Code != http.StatusBadRequest {
|
||
t.Fatalf("缺少证书路径应返回参数错误:%d %s", response.Code, response.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestCertificateImportAPIRejectsInvalidPathJSON(t *testing.T) {
|
||
server := New(Dependencies{
|
||
Config: readyTestConfig(t.TempDir()),
|
||
Repository: &stubDomainRepository{},
|
||
ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||
t.Fatal("无效证书路径请求不应调用导入服务")
|
||
return certificate.IssueResult{}, deployment.Report{}, nil
|
||
},
|
||
})
|
||
|
||
tests := []string{
|
||
`{"certificate_path":"/ssl/cert.pem","private_key_path":"/ssl/key.pem","unexpected":true}`,
|
||
`{"certificate_path":"/ssl/cert.pem","private_key_path":"/ssl/key.pem"}{"certificate_path":"again"}`,
|
||
}
|
||
for _, body := range tests {
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
response := httptest.NewRecorder()
|
||
serveAdminHTTP(server, response, request)
|
||
if response.Code != http.StatusBadRequest {
|
||
t.Fatalf("无效证书路径请求应返回 400,实际为 %d:%s", response.Code, response.Body.String())
|
||
}
|
||
}
|
||
}
|