Files
Felis/game_entrypoint_test.go
T

134 lines
4.8 KiB
Go

package felis
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// The lobby and login gate take their player cap from server.properties, which the
// entrypoint rewrites on every boot over whatever the volume already holds. These run
// the shipped entrypoints the way a pod does (image and volume paths pointed into temp
// dirs, java replaced by a stub that exits) and read the file the server would start on.
// runEntrypoint runs the embedded entrypoint with its runtime dir holding the given
// files and server.properties seeded with props ("" for a first boot), and returns
// server.properties afterwards.
func runEntrypoint(t *testing.T, name, runtimeVar string, files []string, props string, env ...string) string {
t.Helper()
root := t.TempDir()
runtime, data, bin := filepath.Join(root, "image"), filepath.Join(root, "data"), filepath.Join(root, "bin")
for _, f := range files {
p := filepath.Join(runtime, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("jar"), 0o644); err != nil {
t.Fatal(err)
}
}
for _, d := range []string{data, bin} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if props != "" {
if err := os.WriteFile(filepath.Join(data, "server.properties"), []byte(props), 0o644); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(bin, "java"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatal(err)
}
script := readGameStackFile(t, name)
for old, repl := range map[string]string{
runtimeVar: `RUNTIME_DIR="` + runtime + `"`,
`DATA_DIR="/data"`: `DATA_DIR="` + data + `"`,
} {
if !strings.Contains(script, old) {
t.Fatalf("%s no longer sets %s", name, old)
}
script = strings.Replace(script, old, repl, 1)
}
path := filepath.Join(root, "entrypoint.sh")
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
cmd := exec.Command("sh", path)
cmd.Env = append([]string{"PATH=" + bin + ":" + os.Getenv("PATH"), "FELIS_FORWARDING_SECRET=fwd-test"}, env...)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%s failed: %v\n%s", name, err, out)
}
got, err := os.ReadFile(filepath.Join(data, "server.properties"))
if err != nil {
t.Fatal(err)
}
return string(got)
}
// propLines lists the key's lines, so a key written twice shows up as two.
func propLines(props, key string) []string {
var out []string
for _, line := range strings.Split(props, "\n") {
if strings.HasPrefix(line, key+"=") {
out = append(out, line)
}
}
return out
}
func assertProp(t *testing.T, props, key, want string) {
t.Helper()
got := propLines(props, key)
if len(got) != 1 || got[0] != key+"="+want {
t.Errorf("%s: got %q, want exactly [%s=%s]\nserver.properties:\n%s", key, got, key, want, props)
}
}
var lobbyImage = []string{"paper.jar", "plugins/felis-paper.jar", "plugins/LuckPerms.jar"}
func TestLobbyEntrypointLiftsThePlayerCap(t *testing.T) {
t.Run("over the cap Paper wrote on an earlier boot", func(t *testing.T) {
props := runEntrypoint(t, "deploy/lobby/entrypoint.sh", `RUNTIME_DIR="/paper"`, lobbyImage,
"#Minecraft server properties\nmax-players=20\nmotd=Kept as it was\n")
assertProp(t, props, "max-players", "200")
assertProp(t, props, "motd", "Kept as it was")
})
t.Run("on a first boot", func(t *testing.T) {
props := runEntrypoint(t, "deploy/lobby/entrypoint.sh", `RUNTIME_DIR="/paper"`, lobbyImage, "")
assertProp(t, props, "max-players", "200")
assertProp(t, props, "online-mode", "false")
})
}
// The RCON password is arbitrary bytes from a Secret; each of sed's special characters
// has to land in the file as itself when an earlier boot's line is replaced.
func TestLobbyEntrypointWritesTheRconPasswordVerbatim(t *testing.T) {
const password = `a|b\c&d/e`
props := runEntrypoint(t, "deploy/lobby/entrypoint.sh", `RUNTIME_DIR="/paper"`, lobbyImage,
"enable-rcon=false\nrcon.password=stale\n", "RCON_PASSWORD="+password)
assertProp(t, props, "enable-rcon", "true")
assertProp(t, props, "rcon.password", password)
}
var limboImage = []string{"Limbo.jar", "plugins/felis-limbo.jar"}
func TestLimboEntrypointNeverCapsTheGate(t *testing.T) {
t.Run("over a cap left on the volume", func(t *testing.T) {
props := runEntrypoint(t, "deploy/limbo/entrypoint.sh", `RUNTIME_DIR="/limbo"`, limboImage,
"max-players=10\nlevel-name=world;spawn.schem\n")
assertProp(t, props, "max-players", "-1")
assertProp(t, props, "level-name", "world;spawn.schem")
assertProp(t, props, "velocity-modern", "true")
})
t.Run("on a first boot", func(t *testing.T) {
props := runEntrypoint(t, "deploy/limbo/entrypoint.sh", `RUNTIME_DIR="/limbo"`, limboImage, "")
assertProp(t, props, "max-players", "-1")
assertProp(t, props, "forwarding-secrets", "fwd-test")
})
}