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) } }