mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 06:27:53 +02:00
Inserting `version` above a leading `---` put it in a document of its own, so every setting after the marker parsed as a second document and was silently ignored: the installation came up on defaults with a config that looked intact. Both writers - the Go one and upgrade.sh's - now insert after the document start, and the parse-back check still refuses anything a line rewrite cannot do safely. upgrade.sh rewrote config.yaml through a temp file created with the process umask, so a 0600 config came back 0644 on every package upgrade - the file holding the API keys became group/world readable. It now creates the temp file privately and restores the original mode before the atomic replace. A package upgrade also deleted `.update-build-pending`, because rsync --delete only keeps what the exclude list names: the installation forgot a compile it still owed and the next click reported "already up to date". Every one-click update artifact is on the keep list now, asserted against .gitignore so the next one cannot be forgotten the same way.
191 lines
5.8 KiB
Go
191 lines
5.8 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: "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())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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 ""
|
|
}
|