package test import ( "RenewCMS/internal/domain/update" "bytes" "encoding/json" "fmt" "net" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" ) func runBinary(t *testing.T, binary string, workDir string, args ...string) (string, string, int) { t.Helper() command := exec.Command(binary, args...) command.Dir = workDir command.Env = emptyEnvironment() var stdout, stderr bytes.Buffer command.Stdout, command.Stderr = &stdout, &stderr err := command.Run() code := 0 if exitErr, ok := err.(*exec.ExitError); ok { code = exitErr.ExitCode() } else if err != nil { t.Fatal(err) } return stdout.String(), stderr.String(), code } func assertEmptyDirectory(t *testing.T, directory string) { t.Helper() if entries, _ := os.ReadDir(directory); len(entries) != 0 { t.Errorf("the working directory must stay empty, got %d entries", len(entries)) } } func TestCommandDispatcher(t *testing.T) { binary := buildServerBinary(t, "-X main.version=v9.9.9") t.Run("--version prints the version and exits 0", func(t *testing.T) { workDir := t.TempDir() stdout, _, code := runBinary(t, binary, workDir, "--version") if code != 0 || strings.TrimSpace(stdout) != "v9.9.9" { t.Errorf("code = %d, stdout = %q", code, stdout) } assertEmptyDirectory(t, workDir) }) t.Run("an unknown subcommand prints a usage message and does not start the server", func(t *testing.T) { workDir := t.TempDir() stdout, stderr, code := runBinary(t, binary, workDir, "frobnicate") if code == 0 || !strings.Contains(stderr, "usage:") || stdout != "" { t.Errorf("code = %d, stdout = %q, stderr = %q", code, stdout, stderr) } assertEmptyDirectory(t, workDir) }) t.Run("apply-update runs without .env and creates no data directory", func(t *testing.T) { workDir := t.TempDir() _, stderr, code := runBinary(t, binary, workDir, "apply-update") if code == 0 || strings.Contains(stderr, "PUBLIC_URL") || strings.Contains(stderr, ".env") { t.Errorf("code = %d, stderr = %q", code, stderr) } assertEmptyDirectory(t, workDir) }) t.Run("apply-update with its flags never reads .env or opens the database", func(t *testing.T) { if runtime.GOOS == "linux" { t.Skip("on Linux the helper would act on the machine") } workDir := t.TempDir() _, stderr, _ := runBinary(t, binary, workDir, "apply-update", "--binary", filepath.Join(workDir, "bin"), "--data-dir", filepath.Join(workDir, "data")) if strings.Contains(stderr, "PUBLIC_URL") { t.Errorf("stderr = %q", stderr) } if _, err := os.Stat(filepath.Join(workDir, "data")); err == nil { t.Error("no data directory may be created") } }) t.Run("enable-browser-updates and disable-browser-updates refuse off Linux", func(t *testing.T) { if runtime.GOOS == "linux" { t.Skip("on Linux these commands would act on the machine's systemd") } workDir := t.TempDir() _, stderr, code := runBinary(t, binary, workDir, "enable-browser-updates") stdout, _, disableCode := runBinary(t, binary, workDir, "disable-browser-updates") if code == 0 || strings.TrimSpace(stderr) != update.ErrNeedsLinux.Error() { t.Errorf("enable: code = %d, stderr = %q", code, stderr) } if disableCode != 0 || !strings.Contains(stdout, "not enabled") { t.Errorf("disable: code = %d, stdout = %q", disableCode, stdout) } assertEmptyDirectory(t, workDir) }) t.Run("no arguments is the normal server start", func(t *testing.T) { workDir := t.TempDir() _, stderr, code := runBinary(t, binary, workDir) if code == 0 || !strings.Contains(stderr, "PUBLIC_URL") { t.Errorf("code = %d, stderr = %q", code, stderr) } }) } func freeTCPPort(t *testing.T) string { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() return fmt.Sprint(listener.Addr().(*net.TCPAddr).Port) } func TestServerWritesRunningInfoBeforeServing(t *testing.T) { if os.Getenv("RENEWCMS_TEST_SPAWN_SERVER") == "" { t.Skip("spawns the real server, which triggers Windows firewall prompts; set RENEWCMS_TEST_SPAWN_SERVER=1 to run") } binary := buildServerBinary(t, "-X main.version=v9.9.9") workDir := t.TempDir() dataDir := filepath.Join(workDir, "data") port := freeTCPPort(t) command := exec.Command(binary) command.Dir = workDir command.Env = append(emptyEnvironment(), "ENVIRONMENT=development", "PORT="+port, "PUBLIC_URL=http://localhost", "JWT_SECRET=test-secret-for-dispatch", "SMTP_LOGIN=a@example.com", "SMTP_SENDER=a@example.com", "SMTP_PASSWORD=x", "SMTP_HOST=localhost", "SMTP_PORT=2525", "DB_FILE="+filepath.Join(dataDir, "renewcms.db"), "UPLOAD_DIR="+filepath.Join(dataDir, "uploads"), "UPDATE_FEED_URL=https://git.example") var stderr bytes.Buffer command.Stderr = &stderr started := time.Now() if err := command.Start(); err != nil { t.Fatal(err) } defer func() { _ = command.Process.Kill() _, _ = command.Process.Wait() }() healthy := false for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { response, err := http.Get("http://127.0.0.1:" + port + "/healthz") if err == nil { response.Body.Close() healthy = response.StatusCode == http.StatusOK if healthy { break } } } if !healthy { t.Fatalf("the server never answered /healthz: %s", stderr.String()) } content, err := os.ReadFile(filepath.Join(dataDir, update.RunningInfoFileName)) if err != nil { t.Fatalf("update-running.json must exist as soon as /healthz answers: %v", err) } var info update.RunningInfo if err := json.Unmarshal(content, &info); err != nil { t.Fatal(err) } if info.Version != "v9.9.9" || info.Pid != command.Process.Pid || info.StartedAt.Before(started.Add(-2*time.Second)) || info.StartedAt.After(time.Now()) { t.Errorf("info = %+v, pid = %d", info, command.Process.Pid) } }