Files
CyberStrikeAI/cmd/server/build_scripts_test.go
T

224 lines
7.0 KiB
Go

package main
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
// Every `go build` in this repository's scripts has to target the cmd/server package.
// Building the single file cmd/server/main.go was tried once and broke the password reset
// entry: the file-based form silently leaves out sibling files of package main (it was
// update_cli.go that disappeared), and only `run.sh --reset-admin-password` used that
// build path, so nothing noticed until an operator needed it in an emergency. Compiling
// the package can never have that failure mode.
func TestScriptsBuildThePackageNotASingleFile(t *testing.T) {
root := filepath.Join("..", "..")
scripts := []string{filepath.Join(root, "run.sh"), filepath.Join(root, "upgrade.sh")}
more, err := filepath.Glob(filepath.Join(root, "scripts", "*.sh"))
if err != nil {
t.Fatal(err)
}
scripts = append(scripts, more...)
found := 0
for _, script := range scripts {
data, err := os.ReadFile(script)
if err != nil {
t.Fatalf("read %s: %v", script, err)
}
for _, line := range strings.Split(string(data), "\n") {
if !strings.Contains(line, "go build") {
continue
}
found++
if strings.Contains(line, ".go") {
t.Errorf("%s builds a single .go file; build ./cmd/server instead:\n %s",
script, strings.TrimSpace(line))
}
}
}
// A scan that finds no build command at all is looking in the wrong place, and a test
// that cannot see the thing it guards passes for the wrong reason.
if found == 0 {
t.Fatal("no `go build` command found in the scripts")
}
}
func TestUpgradeScriptUpdateConfigVersionPreservesDocumentAndMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("upgrade.sh requires a POSIX shell")
}
bash, err := exec.LookPath("bash")
if err != nil {
t.Skip("bash is not available")
}
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 is not available")
}
root := filepath.Join("..", "..")
scriptPath := filepath.Join(root, "upgrade.sh")
script, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatalf("read %s: %v", scriptPath, err)
}
function := extractShellFunction(t, string(script), "update_config_version")
command := "umask 022\n" + function + "\nCONFIG_FILE=$1\nupdate_config_version \"$2\"\n"
for _, tc := range []struct {
name string
original string
wantDocumentStart bool
}{
{name: "document start", original: "---\nserver:\n port: 8088\n", wantDocumentStart: true},
{name: "commented document start", original: "# local settings\n---\nserver:\n port: 8088\n"},
{name: "directive document start", original: "%YAML 1.1\n---\nserver:\n port: 8088\n"},
{name: "private mode", original: "version: \"v1.0.0\"\nserver:\n port: 8088\n"},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(configPath, []byte(tc.original), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(configPath, 0o600); err != nil {
t.Fatal(err)
}
cmd := exec.Command(bash, "-c", command, "upgrade-version-test", configPath, "v1.2.3")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("update_config_version failed: %v\n%s", err, output)
}
got, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
if tc.wantDocumentStart && !strings.HasPrefix(string(got), "---\n") {
t.Errorf("YAML document start must remain first: %q", got)
}
var parsed struct {
Version string `yaml:"version"`
Server struct {
Port int `yaml:"port"`
} `yaml:"server"`
}
if err := yaml.Unmarshal(got, &parsed); err != nil {
t.Fatalf("updated config is not valid YAML: %v\n%s", err, got)
}
if parsed.Version != "v1.2.3" {
t.Errorf("version = %q, want v1.2.3", parsed.Version)
}
if parsed.Server.Port != 8088 {
t.Errorf("server.port = %d, want 8088", parsed.Server.Port)
}
info, err := os.Stat(configPath)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Errorf("mode = %v, want 0600 preserved", info.Mode().Perm())
}
})
}
}
func TestUpgradeScriptVersionRefusesUnsupportedYAML(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("upgrade.sh requires a POSIX shell")
}
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 unavailable")
}
script, err := os.ReadFile(filepath.Join("..", "..", "upgrade.sh"))
if err != nil {
t.Fatal(err)
}
function := extractShellFunction(t, string(script), "update_config_version")
for _, original := range []string{
"--- {server: {port: 8088}}\n",
"{server: {port: 8088}}\n",
"version: \"old\n version\"\nserver:\n port: 8088\n",
} {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(original), 0600); err != nil {
t.Fatal(err)
}
command := function + "\nCONFIG_FILE=$1\nupdate_config_version v1.2.3\n"
if output, err := exec.Command("bash", "-c", command, "test", path).CombinedOutput(); err != nil {
t.Fatalf("writer failed: %v %s", err, output)
}
if got, err := os.ReadFile(path); err != nil || string(got) != original {
t.Fatalf("unsupported YAML was changed: %q %v", got, err)
}
}
}
// The tarball path syncs with `rsync --delete`, so every artifact a one-click update writes
// at the install root has to be on its keep list. .gitignore is exactly that list of
// machine-local artifacts, so the assertion is derived from it: a new marker cannot be
// forgotten here and silently deleted by the next package upgrade.
func TestUpgradeScriptKeepsEveryOneClickUpdateArtifact(t *testing.T) {
root := filepath.Join("..", "..")
ignore, err := os.ReadFile(filepath.Join(root, ".gitignore"))
if err != nil {
t.Fatal(err)
}
script, err := os.ReadFile(filepath.Join(root, "upgrade.sh"))
if err != nil {
t.Fatal(err)
}
var artifacts []string
inSection := false
for _, line := range strings.Split(string(ignore), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "# One-click update artifacts") {
inSection = true
continue
}
if !inSection {
continue
}
if strings.HasPrefix(line, "#") {
break
}
if line != "" {
artifacts = append(artifacts, line)
}
}
if len(artifacts) == 0 {
t.Fatal("no update artifacts found in .gitignore: the section this test reads has moved")
}
// The script names the binary through a variable; compare against the literal name.
keeps := strings.ReplaceAll(string(script), "${BINARY_NAME}", "cyberstrike-ai")
for _, artifact := range artifacts {
if !strings.Contains(keeps, "--exclude="+artifact) {
t.Errorf("upgrade.sh syncs with rsync --delete but does not keep %s", artifact)
}
}
}
func extractShellFunction(t *testing.T, script, name string) string {
t.Helper()
startMarker := name + "() {"
start := strings.Index(script, startMarker)
if start < 0 {
t.Fatalf("function %s not found", name)
}
lines := strings.Split(script[start:], "\n")
for i := 1; i < len(lines); i++ {
if lines[i] == "}" {
return strings.Join(lines[:i+1], "\n")
}
}
t.Fatalf("function %s has no closing brace", name)
return ""
}