feat(draft): catch split text settings and owner-editable game ports
Two problems a draft can boot fine with but break once an owner changes a
setting, both in the builder's Valheim draft:
- splitSettings: text settings used unquoted on the command line, directly
or through ARGS="-name ${SERVER_NAME}"; ./server $ARGS, so "GBRHosting -
My Server" reaches the game as several arguments. Suggests quoting or a
bash array.
- portSettings: game-port settings whose default isn't in defaultPorts, or
that owners can change although nodes always publish the same container
port. Internal ports (RCON, REST) and {PUBLIC_PORT_n} values are left out.
Both run on drafts only; AUTHORING.md gets the matching rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c75190d8c4
commit
c2b22d3955
4 files changed
+365
No files matched your search
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func textSettings(keys ...string) map[string]any {
|
||||
props := map[string]any{}
|
||||
for _, k := range keys {
|
||||
props[k] = map[string]any{"type": "string"}
|
||||
}
|
||||
return map[string]any{"properties": props}
|
||||
}
|
||||
|
||||
func TestSplitSettingsCatchesTheValheimDraft(t *testing.T) {
|
||||
// What the builder drafted for Valheim: quoted in the assignment, split by the unquoted $ARGS
|
||||
game := map[string]any{
|
||||
"defaultStartupCommand": `ARGS="-name ${SERVER_NAME:-Valheim Server} -port 2456 -world ${WORLD_NAME:-Dedicated}"; ` +
|
||||
`[ -n "$PASSWORD" ] && ARGS="$ARGS -password $PASSWORD"; [ "$CROSSPLAY" = "true" ] && ARGS="$ARGS -crossplay"; ` +
|
||||
`HOME=/server ./valheim_server.x86_64 $ARGS`,
|
||||
"configSchema": textSettings("ServerName", "WorldName", "Password"),
|
||||
}
|
||||
problems := splitSettings(game)
|
||||
got := strings.Join(problems, "\n")
|
||||
if len(problems) != 3 {
|
||||
t.Fatalf("want ServerName, WorldName and Password, got:\n%s", got)
|
||||
}
|
||||
for _, key := range []string{"setting Password", "setting ServerName", "setting WorldName", "inside $ARGS"} {
|
||||
if !strings.Contains(got, key) {
|
||||
t.Errorf("missing %q in:\n%s", key, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettingsAcceptsQuotedAndArrays(t *testing.T) {
|
||||
for _, startup := range []string{
|
||||
`./valheim_server.x86_64 -name "${SERVER_NAME}" -world "$WORLD_NAME" -port 2456`,
|
||||
`args=(-name "$SERVER_NAME" -world "${WORLD_NAME:-Dedicated}"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./valheim_server.x86_64 "${args[@]}"`,
|
||||
`bash -c 'exec ./server -name "$SERVER_NAME"'`,
|
||||
} {
|
||||
game := map[string]any{"defaultStartupCommand": startup, "configSchema": textSettings("ServerName", "WorldName", "Password")}
|
||||
if p := splitSettings(game); len(p) != 0 {
|
||||
t.Errorf("%s\n flagged: %v", startup, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettingsDirectAndArrayMisuse(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`./server -name ${SERVER_NAME}`: "is not in double quotes",
|
||||
`bash -c 'exec ./server -name $SERVER_NAME'`: "is not in double quotes",
|
||||
`args=(-name "$SERVER_NAME"); ./server ${args[@]}`: "inside $args",
|
||||
`args=(-name $SERVER_NAME); ./server "${args[@]}"`: "is not in double quotes",
|
||||
`export OPTS="-name ${SERVER_NAME}" && ./run $OPTS`: "inside $OPTS",
|
||||
}
|
||||
for startup, want := range cases {
|
||||
game := map[string]any{"defaultStartupCommand": startup, "configSchema": textSettings("ServerName")}
|
||||
got := strings.Join(splitSettings(game), "\n")
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("%s\n want %q, got %q", startup, want, got)
|
||||
}
|
||||
}
|
||||
// Numbers and booleans can't contain spaces
|
||||
game := map[string]any{"defaultStartupCommand": `./server -port ${GAME_PORT}`, "configSchema": map[string]any{
|
||||
"properties": map[string]any{"GamePort": map[string]any{"type": "integer"}}}}
|
||||
if p := splitSettings(game); len(p) != 0 {
|
||||
t.Errorf("integer setting flagged: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortSettings(t *testing.T) {
|
||||
game := map[string]any{
|
||||
"defaultPorts": []any{"2456/udp", "2457/udp"},
|
||||
"defaultConfig": map[string]any{
|
||||
"GamePort": json.Number("2456"),
|
||||
"QueryPort": json.Number("27015"),
|
||||
"RCONPort": json.Number("25575"),
|
||||
"PublicPort": "{PUBLIC_PORT_2456}",
|
||||
"Transport": "udp",
|
||||
"Export": json.Number("9"),
|
||||
},
|
||||
"configSchema": map[string]any{"properties": map[string]any{
|
||||
"GamePort": map[string]any{"type": "integer"}, "QueryPort": map[string]any{"type": "integer"},
|
||||
}},
|
||||
}
|
||||
got := strings.Join(portSettings(game), "\n")
|
||||
if !strings.Contains(got, "setting GamePort lets owners change the port") || !strings.Contains(got, "write 2456 directly") {
|
||||
t.Errorf("editable published port not caught:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "setting QueryPort defaults to port 27015, which defaultPorts does not publish") {
|
||||
t.Errorf("unpublished port not caught:\n%s", got)
|
||||
}
|
||||
for _, key := range []string{"RCONPort", "PublicPort", "Transport", "Export"} {
|
||||
if strings.Contains(got, "setting "+key+" ") {
|
||||
t.Errorf("%s should be left alone:\n%s", key, got)
|
||||
}
|
||||
}
|
||||
|
||||
// A published port written by the template itself, not offered to owners, is fine
|
||||
delete(game["configSchema"].(map[string]any)["properties"].(map[string]any), "GamePort")
|
||||
delete(game["defaultConfig"].(map[string]any), "QueryPort")
|
||||
if p := portSettings(game); len(p) != 0 {
|
||||
t.Errorf("got %v", p)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user