Scert/backend/internal/httpapi/certificate_api_test.go

194 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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())
}
}
}