132 lines
6.0 KiB
Go
132 lines
6.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"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/repository"
|
|
)
|
|
|
|
func TestTaskDeploymentResultsIssueAndRead(t *testing.T) {
|
|
server, tasks, _, closeDB := newTaskAPITestServer(t, func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) {
|
|
return certificate.IssueResult{DomainID: id}, deployment.Report{
|
|
Status: "partial",
|
|
Results: []deployment.Result{
|
|
{ServerID: "server-a", Server: "服务器甲", Status: "success", Message: "部署完成", BackupID: "backup-a"},
|
|
{ServerID: "server-b", Server: "服务器乙", Status: "failed", Message: "/etc/scert/private/key.pem: permission denied"},
|
|
},
|
|
}, errors.New("部分服务器部署失败")
|
|
})
|
|
defer closeDB()
|
|
response := httptest.NewRecorder()
|
|
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
|
if response.Code != http.StatusMultiStatus {
|
|
t.Fatalf("部分部署失败响应错误:%d %s", response.Code, response.Body.String())
|
|
}
|
|
items, err := tasks.ListTasks(t.Context(), "", 10)
|
|
if err != nil || len(items) != 1 || items[0].Status != "partial_success" {
|
|
t.Fatalf("部分成功任务记录错误:%+v %v", items, err)
|
|
}
|
|
attempts, err := tasks.ListAttempts(t.Context(), items[0].ID)
|
|
if err != nil || len(attempts) != 1 {
|
|
t.Fatalf("任务尝试记录错误:%+v %v", attempts, err)
|
|
}
|
|
// 模拟应用层持久化的原始错误,确保查询入口也独立脱敏。
|
|
if err := tasks.RecordTaskDeploymentResults(t.Context(), items[0].ID, attempts[0].ID, 1, []repository.TaskDeploymentResult{
|
|
{ResultIndex: 0, ServerID: "server-a", Server: "服务器甲", Status: "success", Message: "部署完成", BackupID: "backup-a"},
|
|
{ResultIndex: 1, ServerID: "server-b", Server: "服务器乙", Status: "failed", Message: "/etc/scert/private/key.pem: permission denied"},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response = httptest.NewRecorder()
|
|
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/"+items[0].ID+"/deployment-results", nil))
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("部署结果读取失败:%d %s", response.Code, response.Body.String())
|
|
}
|
|
var payload struct {
|
|
Items []repository.TaskDeploymentResult `json:"items"`
|
|
}
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(payload.Items) != 2 {
|
|
t.Fatalf("服务器结果数量错误:%+v", payload.Items)
|
|
}
|
|
for _, item := range payload.Items {
|
|
if item.TaskID != items[0].ID || item.AttemptID != attempts[0].ID || item.AttemptNumber != 1 || item.CreatedAt.IsZero() {
|
|
t.Fatalf("部署结果任务关联缺失:%+v", item)
|
|
}
|
|
}
|
|
if payload.Items[0].ServerID != "server-a" || payload.Items[0].BackupID != "backup-a" || payload.Items[0].Status != "success" {
|
|
t.Fatalf("成功结果错误:%+v", payload.Items[0])
|
|
}
|
|
if payload.Items[1].ServerID != "server-b" || payload.Items[1].Status != "failed" || payload.Items[1].Message != "证书部署失败" {
|
|
t.Fatalf("失败结果未脱敏:%+v", payload.Items[1])
|
|
}
|
|
for _, secret := range []string{"/etc/scert/private/key.pem", "permission denied"} {
|
|
if strings.Contains(response.Body.String(), secret) {
|
|
t.Fatalf("部署结果泄露底层错误:%s", response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTaskDeploymentResultsVisibility(t *testing.T) {
|
|
server, tasks, _, closeDB := newTaskAPITestServer(t, nil)
|
|
defer closeDB()
|
|
task := repository.Task{ID: "results-task", DomainID: "domain-1", TaskType: "certificate_issue", Status: "success", Stage: "deploy"}
|
|
if err := tasks.CreateTask(t.Context(), task); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tasks.CreateAttempt(t.Context(), repository.TaskAttempt{ID: "results-attempt", TaskID: task.ID, AttemptNumber: 1, Status: "success", Stage: "deploy"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tasks.RecordTaskDeploymentResults(t.Context(), task.ID, "results-attempt", 1, []repository.TaskDeploymentResult{{ServerID: "private-server", Status: "success"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adminCookie := httpUserSessionCookie(server, "test-admin", repository.UserRoleAdmin)
|
|
outsiderCookie := httpUserSessionCookie(server, "outsider", repository.UserRoleUser)
|
|
memberCookie := httpUserSessionCookie(server, "reader", repository.UserRoleUser)
|
|
if err := server.projectMembers.GrantProjectRole(t.Context(), "test-admin", domain.DefaultProjectID, "reader", domain.ProjectRoleReadOnly); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
cookie *http.Cookie
|
|
status int
|
|
}{
|
|
{"admin", adminCookie, http.StatusOK},
|
|
{"member", memberCookie, http.StatusOK},
|
|
{"outsider", outsiderCookie, http.StatusForbidden},
|
|
{"anonymous", nil, http.StatusUnauthorized},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
response := serveUserRequest(server, tc.cookie, http.MethodGet, "/api/v1/tasks/"+task.ID+"/deployment-results", "")
|
|
if response.Code != tc.status {
|
|
t.Fatalf("部署结果权限响应错误:%d %s", response.Code, response.Body.String())
|
|
}
|
|
if tc.status != http.StatusOK && strings.Contains(response.Body.String(), "private-server") {
|
|
t.Fatal("未授权响应泄露服务器信息")
|
|
}
|
|
})
|
|
}
|
|
if err := server.projectMembers.RevokeProjectRole(t.Context(), "test-admin", domain.DefaultProjectID, "reader", domain.ProjectRoleReadOnly); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := serveUserRequest(server, memberCookie, http.MethodGet, "/api/v1/tasks/"+task.ID+"/deployment-results", "")
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("撤权后旧会话仍可读取部署结果:%d %s", response.Code, response.Body.String())
|
|
}
|
|
visible, err := tasks.ListVisibleTaskDeploymentResults(t.Context(), "reader", task.ID)
|
|
if err != nil || len(visible) != 0 {
|
|
t.Fatalf("仓储读取未实时隔离撤权成员:%+v %v", visible, err)
|
|
}
|
|
}
|