Scert/backend/internal/httpapi/certificate_restore_async_test.go

157 lines
5.7 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 (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
)
func restoreRequest(key string) *http.Request {
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/versions/history-1/restore", nil)
request.Header.Set("Idempotency-Key", key)
return request
}
func TestAsyncRestoreSubmitsPersistedTaskWithoutNestedWorkflow(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
var calls atomic.Int32
var gotDomain, gotVersion string
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
defer cleanup()
server.restoreVersion = func(context.Context, string, string) (certificate.IssueResult, error) {
t.Fatal("异步恢复不得调用兼容同步工作流")
return certificate.IssueResult{}, nil
}
server.restoreVersionTask = func(ctx context.Context, domainID, version string) (certificate.IssueResult, error) {
calls.Add(1)
gotDomain, gotVersion = domainID, version
close(started)
select {
case <-release:
return certificate.IssueResult{DomainID: domainID}, nil
case <-ctx.Done():
return certificate.IssueResult{}, ctx.Err()
}
}
response := httptest.NewRecorder()
serveAdminHTTP(server, response, restoreRequest("restore-async-1"))
task := submittedTask(t, response)
if task.TaskType != "certificate_restore" {
t.Fatalf("恢复任务类型错误:%+v", task)
}
if response.Header().Get("Location") != "/api/v1/tasks/"+task.ID {
t.Fatalf("恢复任务查询地址错误:%s", response.Header().Get("Location"))
}
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("恢复后台任务未开始执行")
}
close(release)
waitTaskWorkersForTest(t, server)
stored, err := tasks.GetTask(t.Context(), task.ID)
if err != nil || stored.Status != "success" {
t.Fatalf("恢复任务未成功收敛:%+v %v", stored, err)
}
if gotDomain != "domain-1" || gotVersion != "history-1" || calls.Load() != 1 {
t.Fatalf("恢复参数或执行次数错误:domain=%q version=%q calls=%d", gotDomain, gotVersion, calls.Load())
}
if !strings.Contains(stored.Message, "本地证书历史版本恢复完成") || !strings.Contains(stored.Message, "远端服务器部署完成") {
t.Fatalf("恢复任务状态文案未说明本地与远端边界:%s", stored.Message)
}
}
func TestAsyncRestoreIdempotencyAndActiveTaskConflict(t *testing.T) {
release := make(chan struct{})
var calls atomic.Int32
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
defer cleanup()
server.restoreVersionTask = func(ctx context.Context, domainID, _ string) (certificate.IssueResult, error) {
calls.Add(1)
select {
case <-release:
return certificate.IssueResult{DomainID: domainID}, nil
case <-ctx.Done():
return certificate.IssueResult{}, ctx.Err()
}
}
first := httptest.NewRecorder()
serveAdminHTTP(server, first, restoreRequest("restore-duplicate"))
task := submittedTask(t, first)
replay := httptest.NewRecorder()
serveAdminHTTP(server, replay, restoreRequest("restore-duplicate"))
if replay.Code != http.StatusConflict || !strings.Contains(replay.Body.String(), task.ID) {
t.Fatalf("活动恢复任务幂等重放未被拒绝:%d %s", replay.Code, replay.Body.String())
}
conflict := httptest.NewRecorder()
serveAdminHTTP(server, conflict, restoreRequest("restore-other"))
if conflict.Code != http.StatusConflict {
t.Fatalf("同域名活动恢复任务未被拒绝:%d %s", conflict.Code, conflict.Body.String())
}
close(release)
waitTaskWorkersForTest(t, server)
stored, err := tasks.GetTask(t.Context(), task.ID)
if err != nil || stored.Status != "success" || calls.Load() != 1 {
t.Fatalf("幂等或冲突检查影响恢复任务:%+v %v calls=%d", stored, err, calls.Load())
}
}
func TestAsyncRestoreFailureDoesNotExposePrivatePath(t *testing.T) {
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
defer cleanup()
server.restoreVersionTask = func(context.Context, string, string) (certificate.IssueResult, error) {
return certificate.IssueResult{}, errors.New("读取私钥失败:/etc/scert/private/live/key.pem")
}
response := httptest.NewRecorder()
serveAdminHTTP(server, response, restoreRequest("restore-failure"))
task := submittedTask(t, response)
waitTaskWorkersForTest(t, server)
stored, err := tasks.GetTask(t.Context(), task.ID)
if err != nil || stored.Status != "failed" {
t.Fatalf("恢复失败任务未收敛:%+v %v", stored, err)
}
if strings.Contains(stored.Message, "/etc/scert/private") || strings.Contains(response.Body.String(), "/etc/scert/private") {
t.Fatal("恢复失败结果泄露私钥路径")
}
}
func TestAsyncRestoreCancellationStopsWorkflow(t *testing.T) {
started := make(chan struct{})
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
defer cleanup()
server.restoreVersionTask = func(ctx context.Context, domainID, _ string) (certificate.IssueResult, error) {
close(started)
<-ctx.Done()
return certificate.IssueResult{DomainID: domainID}, ctx.Err()
}
response := httptest.NewRecorder()
serveAdminHTTP(server, response, restoreRequest("restore-cancel"))
task := submittedTask(t, response)
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("恢复任务未开始执行")
}
cancel := httptest.NewRecorder()
serveAdminHTTP(server, cancel, httptest.NewRequest(http.MethodDelete, "/api/v1/tasks/"+task.ID, nil))
if cancel.Code != http.StatusAccepted {
t.Fatalf("取消恢复任务失败:%d %s", cancel.Code, cancel.Body.String())
}
waitTaskWorkersForTest(t, server)
stored, err := tasks.GetTask(t.Context(), task.ID)
if err != nil || stored.Status != "cancelled" {
t.Fatalf("恢复任务取消后状态错误:%+v %v", stored, err)
}
}