Primer paso hacia separar el motor (herramienta genérica) de cada instancia (contenido de un deployment): Version deja de ser const para poder inyectarse con -ldflags en `make build` (git describe, sin tags aún), y el comando nuevo bootstrap-instance genera el esqueleto mínimo de una instancia (zola.toml, content/<sección>/_index.md, admins.json vacío, .clusterlog-version) reusando saveAdmins() en vez de duplicar su formato. Documentado en CLI.md; el empaquetado de templates/tema y el CI de instancia quedan para cuando el motor publique releases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
package clusterlog
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newBootstrapApp(t *testing.T, dryRun bool) *App {
|
|
t.Helper()
|
|
return New(Options{DryRun: dryRun, JSON: true, Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, Executable: "clusterlog"})
|
|
}
|
|
|
|
func TestBootstrapInstanceCreatesExpectedFiles(t *testing.T) {
|
|
base := t.TempDir()
|
|
out := filepath.Join(base, "instancia-nueva")
|
|
app := newBootstrapApp(t, false)
|
|
|
|
err := app.runBootstrapInstance([]string{"--output", out, "--product-name", "Bitácora de Prueba", "--base-url", "https://clusterlog.prueba.org"})
|
|
if err != nil {
|
|
t.Fatalf("runBootstrapInstance falló: %v", err)
|
|
}
|
|
|
|
for _, section := range scaffoldSections {
|
|
path := filepath.Join(out, "content", section, "_index.md")
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Fatalf("falta %s: %v", path, statErr)
|
|
}
|
|
}
|
|
|
|
zolaData, err := os.ReadFile(filepath.Join(out, "zola.toml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zola := string(zolaData)
|
|
if !strings.Contains(zola, `base_url = "https://clusterlog.prueba.org"`) {
|
|
t.Fatalf("zola.toml no tiene el base_url esperado: %s", zola)
|
|
}
|
|
if !strings.Contains(zola, `title = "Bitácora de Prueba"`) {
|
|
t.Fatalf("zola.toml no tiene el title esperado: %s", zola)
|
|
}
|
|
if !strings.Contains(zola, `product_name = "Bitácora de Prueba"`) {
|
|
t.Fatalf("zola.toml no tiene product_name esperado: %s", zola)
|
|
}
|
|
|
|
var registry AdminRegistry
|
|
adminsData, err := os.ReadFile(filepath.Join(out, "data", "admins.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(adminsData, ®istry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if registry.Version != 1 || len(registry.Admins) != 0 || registry.ReviewPolicy.Default != "all-active" {
|
|
t.Fatalf("registro de admins inesperado: %+v", registry)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(out, "config", "allowed_signers")); err != nil {
|
|
t.Fatalf("falta config/allowed_signers: %v", err)
|
|
}
|
|
|
|
versionData, err := os.ReadFile(filepath.Join(out, ".clusterlog-version"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if trimmed := strings.TrimSpace(string(versionData)); trimmed != Version {
|
|
t.Fatalf(".clusterlog-version = %q, se esperaba %q", trimmed, Version)
|
|
}
|
|
|
|
for _, dir := range []string{"review-events", "task-events"} {
|
|
if _, err := os.Stat(filepath.Join(out, "data", dir, ".gitkeep")); err != nil {
|
|
t.Fatalf("falta data/%s/.gitkeep: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(out, "README.md")); err != nil {
|
|
t.Fatalf("falta README.md: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(out, ".gitignore")); err != nil {
|
|
t.Fatalf("falta .gitignore: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapInstanceScaffoldValidatesClean(t *testing.T) {
|
|
base := t.TempDir()
|
|
out := filepath.Join(base, "instancia-nueva")
|
|
app := newBootstrapApp(t, false)
|
|
if err := app.runBootstrapInstance([]string{"--output", out}); err != nil {
|
|
t.Fatalf("runBootstrapInstance falló: %v", err)
|
|
}
|
|
|
|
report := ValidationReport{Checks: map[string]int{}, Errors: []ValidationIssue{}, Warnings: []ValidationIssue{}}
|
|
validateApp := &App{}
|
|
registry, err := loadAdmins(out)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
validateApp.validateAdmins(out, registry, &report)
|
|
pages := validateApp.validatePages(out, registry, &report)
|
|
validateApp.validateEvents(out, registry, pages, &report)
|
|
if len(report.Errors) != 0 {
|
|
t.Fatalf("el scaffold recién creado no debería tener errores de validate: %+v", report.Errors)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapInstanceRefusesNonEmptyOutput(t *testing.T) {
|
|
base := t.TempDir()
|
|
out := filepath.Join(base, "instancia-existente")
|
|
if err := os.MkdirAll(out, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustWriteTestFile(t, filepath.Join(out, "algo.txt"), "ya había algo aquí")
|
|
|
|
app := newBootstrapApp(t, false)
|
|
err := app.runBootstrapInstance([]string{"--output", out})
|
|
if err == nil {
|
|
t.Fatal("se esperaba un error por directorio no vacío")
|
|
}
|
|
cliErr, ok := err.(*CLIError)
|
|
if !ok || cliErr.Kind != "output_not_empty" {
|
|
t.Fatalf("se esperaba output_not_empty, se obtuvo: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapInstanceDryRunWritesNothing(t *testing.T) {
|
|
base := t.TempDir()
|
|
out := filepath.Join(base, "instancia-dry-run")
|
|
app := newBootstrapApp(t, true)
|
|
if err := app.runBootstrapInstance([]string{"--output", out}); err != nil {
|
|
t.Fatalf("runBootstrapInstance --dry-run falló: %v", err)
|
|
}
|
|
if _, err := os.Stat(out); err == nil {
|
|
t.Fatal("--dry-run no debería haber creado el directorio de salida")
|
|
}
|
|
}
|
|
|
|
func TestBootstrapInstanceRequiresOutput(t *testing.T) {
|
|
app := newBootstrapApp(t, false)
|
|
err := app.runBootstrapInstance([]string{})
|
|
if err == nil {
|
|
t.Fatal("se esperaba un error por --output faltante")
|
|
}
|
|
cliErr, ok := err.(*CLIError)
|
|
if !ok || cliErr.Kind != "missing_output" {
|
|
t.Fatalf("se esperaba missing_output, se obtuvo: %v", err)
|
|
}
|
|
}
|