package main import ( "os" "path/filepath" "runtime" "strings" "testing" "sigs.k8s.io/yaml" ) // readYAML parses a paper-global.yml into a nested map for assertions. func readYAML(t *testing.T, path string) map[string]any { t.Helper() b, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } var m map[string]any if err := yaml.Unmarshal(b, &m); err != nil { t.Fatalf("parse %s: %v", path, err) } return m } // velocityBlock digs out proxies.velocity from a parsed paper-global.yml. func velocityBlock(t *testing.T, root map[string]any) map[string]any { t.Helper() proxies, ok := root["proxies"].(map[string]any) if !ok { t.Fatalf("no proxies map: %v", root) } vel, ok := proxies["velocity"].(map[string]any) if !ok { t.Fatalf("no proxies.velocity map: %v", proxies) } return vel } // An empty secret must touch nothing: a proxy that is not in modern mode // provisions no Secret, and the init must not wedge the pod over it. func TestWritePaperForwardingEmptySecretIsNoop(t *testing.T) { dir := t.TempDir() if err := writePaperForwarding(dir, ""); err != nil { t.Fatalf("writePaperForwarding: %v", err) } if _, err := os.Stat(filepath.Join(dir, "config", "paper-global.yml")); !os.IsNotExist(err) { t.Errorf("paper-global.yml should not exist, stat err = %v", err) } if _, err := os.Stat(filepath.Join(dir, "server.properties")); !os.IsNotExist(err) { t.Errorf("server.properties should not exist, stat err = %v", err) } } // A fresh data dir gets a complete velocity block and an offline server.properties. func TestWritePaperForwardingFreshDir(t *testing.T) { dir := t.TempDir() if err := writePaperForwarding(dir, "s3cr3t"); err != nil { t.Fatalf("writePaperForwarding: %v", err) } vel := velocityBlock(t, readYAML(t, filepath.Join(dir, "config", "paper-global.yml"))) if vel["enabled"] != true { t.Errorf("velocity.enabled = %v, want true", vel["enabled"]) } if vel["online-mode"] != true { t.Errorf("velocity.online-mode = %v, want true", vel["online-mode"]) } if vel["secret"] != "s3cr3t" { t.Errorf("velocity.secret = %v, want s3cr3t", vel["secret"]) } props, err := os.ReadFile(filepath.Join(dir, "server.properties")) if err != nil { t.Fatalf("read server.properties: %v", err) } if !strings.Contains(string(props), "online-mode=false") { t.Errorf("server.properties missing online-mode=false:\n%s", props) } } // A pre-existing paper-global.yml (as Paper expands it on first boot) must keep // all of its other keys — clobbering them would silently reset the user's tuning // on every restart. This is the whole reason the writer merges rather than // overwrites. func TestWritePaperGlobalPreservesExistingKeys(t *testing.T) { dir := t.TempDir() cfg := filepath.Join(dir, "config") if err := os.MkdirAll(cfg, 0o755); err != nil { t.Fatal(err) } existing := []byte("proxies:\n velocity:\n enabled: false\n secret: OLD\nchunk-loading:\n autoconfig-send-distance: true\nmisc:\n max-joins-per-tick: 5\n") if err := os.WriteFile(filepath.Join(cfg, "paper-global.yml"), existing, 0o644); err != nil { t.Fatal(err) } if err := writePaperGlobal(dir, "NEW"); err != nil { t.Fatalf("writePaperGlobal: %v", err) } root := readYAML(t, filepath.Join(cfg, "paper-global.yml")) vel := velocityBlock(t, root) if vel["enabled"] != true || vel["secret"] != "NEW" { t.Errorf("velocity not updated: %v", vel) } // Unrelated trees survive. if _, ok := root["chunk-loading"].(map[string]any); !ok { t.Errorf("chunk-loading tree lost: %v", root) } misc, ok := root["misc"].(map[string]any) if !ok { t.Fatalf("misc tree lost: %v", root) } // sigs.k8s.io/yaml decodes numbers via JSON, so 5 arrives as float64(5). if misc["max-joins-per-tick"] != float64(5) { t.Errorf("misc.max-joins-per-tick = %v, want 5", misc["max-joins-per-tick"]) } } // online-mode=false must be forced while every other property line — comments // included — is left untouched. func TestForceServerPropertyOfflinePreservesOthers(t *testing.T) { dir := t.TempDir() existing := "#Minecraft server properties\nmotd=Hello World\nonline-mode=true\ndifficulty=hard\n" if err := os.WriteFile(filepath.Join(dir, "server.properties"), []byte(existing), 0o644); err != nil { t.Fatal(err) } if err := forceServerPropertyOffline(dir); err != nil { t.Fatalf("forceServerPropertyOffline: %v", err) } got, err := os.ReadFile(filepath.Join(dir, "server.properties")) if err != nil { t.Fatal(err) } s := string(got) if strings.Contains(s, "online-mode=true") { t.Errorf("online-mode=true not replaced:\n%s", s) } if !strings.Contains(s, "online-mode=false") { t.Errorf("online-mode=false not set:\n%s", s) } for _, keep := range []string{"#Minecraft server properties", "motd=Hello World", "difficulty=hard"} { if !strings.Contains(s, keep) { t.Errorf("lost line %q:\n%s", keep, s) } } } // upsertProperty appends when the key is absent and does not grow blank lines. func TestUpsertPropertyAppends(t *testing.T) { got := string(upsertProperty([]byte("motd=hi"), "online-mode", "false")) if got != "motd=hi\nonline-mode=false\n" { t.Errorf("append form wrong: %q", got) } empty := string(upsertProperty(nil, "online-mode", "false")) if empty != "online-mode=false\n" { t.Errorf("empty form wrong: %q", empty) } // A commented key must not count as present. commented := string(upsertProperty([]byte("#online-mode=true\n"), "online-mode", "false")) if !strings.Contains(commented, "#online-mode=true") || !strings.Contains(commented, "\nonline-mode=false\n") { t.Errorf("comment mishandled: %q", commented) } } // The written files must be group/world writable so a non-root main container can // rewrite them. chmod semantics are POSIX-only, so this asserts on non-Windows. func TestWriteForwardingFileModes(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("POSIX file modes not represented on Windows") } dir := t.TempDir() if err := writePaperForwarding(dir, "x"); err != nil { t.Fatal(err) } for _, p := range []string{ filepath.Join(dir, "config", "paper-global.yml"), filepath.Join(dir, "server.properties"), } { fi, err := os.Stat(p) if err != nil { t.Fatal(err) } if fi.Mode().Perm() != fwdFileMode { t.Errorf("%s mode = %o, want %o", p, fi.Mode().Perm(), fwdFileMode) } } }