package repository import ( "context" "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 TestServerRepositoryCRUDAndDomainAssociations(t *testing.T) { dir := t.TempDir() cfg := config.Default(dir).Database cfg.DSN = filepath.Join(dir, "data", "servers.db") db, err := database.Open(context.Background(), cfg) if err != nil { t.Fatal(err) } defer db.Close() if err := database.Migrate(context.Background(), db.DB, "sqlite"); err != nil { t.Fatal(err) } repo := NewSQLServerRepository(db.DB) server := deployment.Server{ ID: "server-1", Name: "生产服务器", Address: "192.0.2.10", Port: 22, Username: "deploy", AuthMethod: deployment.SSHAuthPassword, PasswordRef: "secret://ssh/password/server-1", HostKeyFingerprint: "SHA256:test", CertificatePath: "/etc/scert/cert.pem", PrivateKeyPath: "/etc/scert/key.pem", CertificateChainPath: "/etc/scert/fullchain.pem", WebServer: deployment.WebServerNginx, } if err := repo.CreateServer(context.Background(), server); err != nil { t.Fatalf("创建服务器失败:%v", err) } loaded, err := repo.GetServerByID(context.Background(), server.ID) if err != nil { t.Fatalf("读取服务器失败:%v", err) } if loaded.Name != server.Name || loaded.ValidationCommand[0] != "nginx" || loaded.ValidationCommand[1] != "-t" || loaded.ReloadCommand[0] != "nginx" || loaded.CreatedAt.IsZero() { t.Fatalf("服务器内容错误:%+v", loaded) } server.Name = "生产服务器-更新" server.WebServer = deployment.WebServerCaddy server.ReloadCommand = nil if err := repo.UpdateServer(context.Background(), server); err != nil { t.Fatalf("更新服务器失败:%v", err) } updated, err := repo.GetServerByID(context.Background(), server.ID) if err != nil { t.Fatalf("load updated server: %v", err) } if updated.CreatedAt.IsZero() || updated.UpdatedAt.IsZero() || updated.UpdatedAt.Before(updated.CreatedAt) { t.Fatalf("updated server timestamps are invalid: %+v", updated) } item, err := domain.NewDomain("example.test", domain.CertificateSingle, nil) if err != nil { t.Fatal(err) } item.ID = "domain-1" if err := NewSQLDomainRepository(db.DB).Create(t.Context(), item); err != nil { t.Fatal(err) } if err := repo.ReplaceDomainServers(context.Background(), "domain-1", []string{server.ID, server.ID}); err != nil { t.Fatalf("保存域名服务器关联失败:%v", err) } associated, err := repo.ListServersByDomainID(context.Background(), "domain-1") if err != nil || len(associated) != 1 || associated[0].WebServer != deployment.WebServerCaddy { t.Fatalf("域名服务器关联错误:%v %+v", err, associated) } servers, err := repo.ListServers(context.Background()) if err != nil || len(servers) != 1 { t.Fatalf("服务器列表错误:%v %+v", err, servers) } if err := repo.DeleteServer(context.Background(), server.ID); err != nil { t.Fatalf("删除服务器失败:%v", err) } associated, err = repo.ListServersByDomainID(context.Background(), "domain-1") if err != nil { t.Fatalf("读取删除后的域名服务器关联失败:%v", err) } if len(associated) != 0 { t.Fatalf("删除服务器后不应保留悬空关联:%+v", associated) } } func TestServerRepositoryRejectsInvalidServer(t *testing.T) { db, err := database.Open(context.Background(), config.DatabaseConfig{ Driver: "sqlite", DSN: ":memory:", MaxOpenConns: 1, MaxIdleConns: 1, }) if err != nil { t.Fatal(err) } defer db.Close() if err := database.Migrate(context.Background(), db.DB, "sqlite"); err != nil { t.Fatal(err) } if err := NewSQLServerRepository(db.DB).CreateServer(context.Background(), deployment.Server{}); err == nil { t.Fatal("无效服务器配置应被拒绝") } } func TestServerRepositoryRejectsUnsupportedAuthentication(t *testing.T) { db := openServerTestDatabase(t) defer db.Close() repo := NewSQLServerRepository(db.DB) server := validTestServer() server.ID = "server-private-key-rejected" server.AuthMethod = deployment.SSHAuthMethod("unsupported") if err := repo.CreateServer(t.Context(), server); err == nil { t.Fatal("server repository must reject unsupported authentication") } } func TestReplaceDomainServersRejectsUnknownServerWithoutChangingAssociations(t *testing.T) { dir := t.TempDir() cfg := config.Default(dir).Database cfg.DSN = filepath.Join(dir, "data", "association.db") db, err := database.Open(context.Background(), cfg) if err != nil { t.Fatal(err) } defer db.Close() if err := database.Migrate(context.Background(), db.DB, "sqlite"); err != nil { t.Fatal(err) } repo := NewSQLServerRepository(db.DB) server := deployment.Server{ID: "server-1", Name: "服务器", Address: "192.0.2.20", Username: "deploy", AuthMethod: deployment.SSHAuthPassword, PasswordRef: "secret://ssh/password/1", HostKeyFingerprint: "SHA256:test", CertificatePath: "/etc/scert/cert.pem", PrivateKeyPath: "/etc/scert/key.pem", CertificateChainPath: "/etc/scert/fullchain.pem", WebServer: deployment.WebServerNginx} if err := repo.CreateServer(context.Background(), server); err != nil { t.Fatal(err) } item, err := domain.NewDomain("association.example.test", domain.CertificateSingle, nil) if err != nil { t.Fatal(err) } item.ID = "domain-1" if err := NewSQLDomainRepository(db.DB).Create(t.Context(), item); err != nil { t.Fatal(err) } if err := repo.ReplaceDomainServers(context.Background(), "domain-1", []string{server.ID}); err != nil { t.Fatal(err) } if err := repo.ReplaceDomainServers(context.Background(), "domain-1", []string{"missing"}); err == nil { t.Fatal("未知服务器应被拒绝") } items, err := repo.ListServersByDomainID(context.Background(), "domain-1") if err != nil || len(items) != 1 || items[0].ID != server.ID { t.Fatalf("失败后原关联应保持不变:%v %+v", err, items) } } func openServerTestDatabase(t *testing.T) *database.Database { t.Helper() directory := t.TempDir() cfg := config.Default(directory).Database cfg.DSN = filepath.Join(directory, "data", "server-test.db") db, err := database.Open(t.Context(), cfg) if err != nil { t.Fatalf("打开服务器测试数据库失败:%v", err) } if err := database.Migrate(t.Context(), db.DB, "sqlite"); err != nil { _ = db.Close() t.Fatalf("迁移服务器测试数据库失败:%v", err) } return db } func validTestServer() deployment.Server { return deployment.Server{ Name: "测试服务器", Address: "192.0.2.70", Port: 22, Username: "deploy", AuthMethod: deployment.SSHAuthPassword, PasswordRef: "secret://ssh/password/test", HostKeyFingerprint: "SHA256:test", CertificatePath: "/etc/scert/cert.pem", PrivateKeyPath: "/etc/scert/key.pem", CertificateChainPath: "/etc/scert/fullchain.pem", WebServer: deployment.WebServerNginx, } }