222 lines
10 KiB
Go
222 lines
10 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||
)
|
||
|
||
func TestProjectDomainRepositorySQLite(t *testing.T) {
|
||
cfg := config.Default(t.TempDir()).Database
|
||
cfg.DSN = filepath.Join(t.TempDir(), "project-domains.db")
|
||
db, err := database.Open(t.Context(), cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { _ = db.Close() })
|
||
if err := database.Migrate(t.Context(), db.DB, cfg.Driver); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, user := range []struct{ id, role string }{
|
||
{"root-domain", UserRoleSuperAdmin}, {"domain-manager", UserRoleUser}, {"domain-outsider", UserRoleUser},
|
||
} {
|
||
if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, `INSERT INTO users
|
||
(id, username, password_hash, role, status) VALUES (?, ?, 'test-only-no-login', ?, 'active')`), user.id, user.id, user.role); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
projects := NewSQLProjectRepository(db.DB, cfg.Driver)
|
||
if err := projects.CreateProject(t.Context(), domain.Project{ID: "domain-project", Name: "域名项目"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
writer := NewSQLProjectMemberRepository(db.DB, cfg.Driver)
|
||
if err := writer.GrantProjectRole(t.Context(), "root-domain", "domain-project", "domain-manager", domain.ProjectRoleCertificateAdmin); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
item, err := domain.NewDomain("managed.example.test", domain.CertificateSingle, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
item.ID = "managed-domain"
|
||
item.ChallengeType = domain.ChallengeHTTP01
|
||
item.ProjectID = "domain-project"
|
||
if err := writer.CreateProjectDomain(t.Context(), "domain-manager", item); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
stored, err := NewSQLDomainRepository(db.DB, cfg.Driver).GetByID(t.Context(), item.ID)
|
||
if err == nil && stored.ChallengeType != domain.ChallengeHTTP01 {
|
||
t.Fatal("项目域名未保存 HTTP 验证方式")
|
||
}
|
||
if err != nil || stored.ProjectID != item.ProjectID {
|
||
t.Fatalf("项目域名未按真实归属保存:%+v,%v", stored, err)
|
||
}
|
||
if err := writer.CreateProjectDomain(t.Context(), "domain-outsider", item); !errors.Is(err, ErrProjectAccessDenied) {
|
||
t.Fatalf("越权创建应拒绝:%v", err)
|
||
}
|
||
item.Name = "updated.example.test"
|
||
item.ChallengeType = domain.ChallengeDNS01
|
||
if err := writer.UpdateProjectDomain(t.Context(), "domain-manager", item); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := NewSQLDomainRepository(db.DB, cfg.Driver).GetByID(t.Context(), item.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
visible, err := writer.GetVisibleDomain(t.Context(), "domain-manager", item.ID)
|
||
if err != nil || visible.ChallengeType != domain.ChallengeDNS01 {
|
||
t.Fatal("项目查询未返回更新后的验证方式:", err)
|
||
}
|
||
serverRepo := NewSQLServerRepository(db.DB, cfg.Driver)
|
||
server := deployment.Server{
|
||
ResourceScope: domain.ResourceScope{ProjectID: item.ProjectID},
|
||
ID: "domain-delete-server",
|
||
Name: "domain delete test server",
|
||
Address: "192.0.2.10",
|
||
Port: 22,
|
||
Username: "deploy",
|
||
HostKeyFingerprint: "SHA256:test",
|
||
PasswordRef: "secret://ssh/domain-delete-server",
|
||
CertificatePath: "/etc/scert/{domain}/cert.pem",
|
||
PrivateKeyPath: "/etc/scert/{domain}/key.pem",
|
||
CertificateChainPath: "/etc/scert/{domain}/fullchain.pem",
|
||
WebServer: deployment.WebServerNginx,
|
||
}
|
||
if err := serverRepo.CreateServer(t.Context(), server); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := serverRepo.ReplaceDomainServers(t.Context(), item.ID, []string{server.ID}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
otherItem, err := domain.NewDomain("surviving.example.test", domain.CertificateSingle, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
otherItem.ID = "surviving-domain"
|
||
otherItem.ProjectID = item.ProjectID
|
||
if err := writer.CreateProjectDomain(t.Context(), "domain-manager", otherItem); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := serverRepo.ReplaceDomainServers(t.Context(), otherItem.ID, []string{server.ID}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
taskRepo := NewSQLTaskRepository(db.DB, cfg.Driver)
|
||
if err := taskRepo.CreateTask(t.Context(), Task{HistoryScope: HistoryScope{ProjectID: item.ProjectID, ScopeType: ScopeProject}, ID: "domain-active-task", DomainID: item.ID, TaskType: "certificate_issue", Status: "running"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.DeleteProjectDomain(t.Context(), "domain-manager", item.ID); !errors.Is(err, ErrProjectDomainBusy) {
|
||
t.Fatalf("在途任务下删除应拒绝:%v", err)
|
||
}
|
||
if _, err := db.DB.ExecContext(context.Background(), Rebind(cfg.Driver, `UPDATE tasks SET status = 'success' WHERE id = ?`), "domain-active-task"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
activeStatuses := []string{"queued", "running", "retry_wait", "cancelling"}
|
||
taskTypes := []string{"certificate_issue", "certificate_validate", "certificate_deploy", "certificate_import", "certificate_restore", "renewal_run"}
|
||
for _, taskType := range taskTypes {
|
||
for _, status := range activeStatuses {
|
||
taskID := "domain-matrix-task-" + taskType + "-" + status
|
||
if err := taskRepo.CreateTask(t.Context(), Task{HistoryScope: HistoryScope{ProjectID: item.ProjectID, ScopeType: ScopeProject}, ID: taskID, DomainID: item.ID, TaskType: taskType, Status: status}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.DeleteProjectDomain(t.Context(), "domain-manager", item.ID); !errors.Is(err, ErrProjectDomainBusy) {
|
||
t.Fatalf("delete should reject for task type %s and status %s: %v", taskType, status, err)
|
||
}
|
||
if _, err := NewSQLDomainRepository(db.DB).GetByID(t.Context(), item.ID); err != nil {
|
||
t.Fatalf("domain disappeared after rejected delete: %v", err)
|
||
}
|
||
associated, err := serverRepo.ListServersByDomainID(t.Context(), item.ID)
|
||
if err != nil || len(associated) != 1 || associated[0].ID != server.ID {
|
||
t.Fatalf("server binding changed after rejected delete: %+v, %v", associated, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, "UPDATE tasks SET status = 'partial_success' WHERE id = ?"), taskID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
for _, status := range []string{"failed", "cancelled"} {
|
||
taskID := "domain-terminal-task-" + status
|
||
if err := taskRepo.CreateTask(t.Context(), Task{HistoryScope: HistoryScope{ProjectID: item.ProjectID, ScopeType: ScopeProject}, ID: taskID, DomainID: item.ID, TaskType: "certificate_issue", Status: status}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
trigger := `CREATE TRIGGER reject_project_domain_audit BEFORE INSERT ON audit_logs BEGIN SELECT RAISE(ABORT, 'audit-failure'); END`
|
||
for _, status := range []string{"retry_wait", "cancelling"} {
|
||
taskID := "domain-active-task-" + status
|
||
if err := taskRepo.CreateTask(t.Context(), Task{HistoryScope: HistoryScope{ProjectID: item.ProjectID, ScopeType: ScopeProject}, ID: taskID, DomainID: item.ID, TaskType: "certificate_deploy", Status: status}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.DeleteProjectDomain(t.Context(), "domain-manager", item.ID); !errors.Is(err, ErrProjectDomainBusy) {
|
||
t.Fatalf("任务状态 %s 下删除应拒绝:%v", status, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, "UPDATE tasks SET status = 'partial_success' WHERE id = ?"), taskID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), trigger); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.UpdateProjectDomain(t.Context(), "domain-manager", item); err == nil {
|
||
t.Fatal("审计失败时更新域名不应成功")
|
||
}
|
||
stored, err = NewSQLDomainRepository(db.DB).GetByID(t.Context(), item.ID)
|
||
if err != nil || stored.Name != "updated.example.test" {
|
||
t.Fatalf("审计失败后域名更新未回滚:%+v,%v", stored, err)
|
||
}
|
||
newItem, err := domain.NewDomain("audit-create.example.test", domain.CertificateSingle, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
newItem.ID = "audit-create-domain"
|
||
newItem.ProjectID = "domain-project"
|
||
if err := writer.CreateProjectDomain(t.Context(), "domain-manager", newItem); err == nil {
|
||
t.Fatal("审计失败时创建域名不应成功")
|
||
}
|
||
if _, err := NewSQLDomainRepository(db.DB).GetByID(t.Context(), newItem.ID); !errors.Is(err, sql.ErrNoRows) {
|
||
t.Fatalf("审计失败后域名创建未回滚:%v", err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), `DROP TRIGGER reject_project_domain_audit`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), trigger); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.DeleteProjectDomain(t.Context(), "domain-manager", item.ID); err == nil {
|
||
t.Fatal("审计失败时删除域名不应成功")
|
||
}
|
||
if _, err := NewSQLDomainRepository(db.DB).GetByID(t.Context(), item.ID); err != nil {
|
||
t.Fatalf("审计失败后域名删除未回滚:%v", err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), `DROP TRIGGER reject_project_domain_audit`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := writer.DeleteProjectDomain(t.Context(), "domain-manager", item.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var count int
|
||
if err := db.DB.QueryRowContext(t.Context(), Rebind(cfg.Driver, `SELECT COUNT(*) FROM audit_logs WHERE resource_id = ? AND status = 'success'`), item.ID).Scan(&count); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if count != 3 {
|
||
t.Fatalf("域名成功操作审计数量错误:%d", count)
|
||
}
|
||
if _, err := NewSQLDomainRepository(db.DB, cfg.Driver).GetByID(t.Context(), item.ID); !errors.Is(err, sql.ErrNoRows) {
|
||
t.Fatalf("删除后域名应不存在:%v", err)
|
||
}
|
||
|
||
if _, err := NewSQLDomainRepository(db.DB).GetByID(t.Context(), otherItem.ID); err != nil {
|
||
t.Fatalf("surviving domain was removed with deleted domain: %v", err)
|
||
}
|
||
if _, err := serverRepo.GetServerByID(t.Context(), server.ID); err != nil {
|
||
t.Fatalf("server was removed with deleted domain: %v", err)
|
||
}
|
||
associated, err := serverRepo.ListServersByDomainID(t.Context(), otherItem.ID)
|
||
if err != nil || len(associated) != 1 || associated[0].ID != server.ID {
|
||
t.Fatalf("surviving domain binding was removed: %+v, %v", associated, err)
|
||
}
|
||
}
|