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 TestSplitSettingsCatchesAnArgsString(t *testing.T) { // A real draft: quoted in the assignment, split by the unquoted $ARGS game := map[string]any{ "defaultStartupCommand": `ARGS="-name ${SERVER_NAME:-Example Server} -port 30000 -world ${WORLD_NAME:-Dedicated}"; ` + `[ -n "$PASSWORD" ] && ARGS="$ARGS -password $PASSWORD"; [ "$CROSSPLAY" = "true" ] && ARGS="$ARGS -crossplay"; ` + `HOME=/server ./example_server $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{ `./example_server -name "${SERVER_NAME}" -world "$WORLD_NAME" -port 30000`, `args=(-name "$SERVER_NAME" -world "${WORLD_NAME:-Dedicated}"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./example_server "${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{"30000/udp", "30001/udp"}, "defaultConfig": map[string]any{ "GamePort": json.Number("30000"), "QueryPort": json.Number("27015"), "RCONPort": json.Number("25575"), "PublicPort": "{PUBLIC_PORT_30000}", "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 30000 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) } } func TestUndocumentedOptionsCatchesMaxPlayers(t *testing.T) { // A real draft's startup: every option is documented except -maxplayers game := map[string]any{"defaultStartupCommand": `args=(-name "${SERVER_NAME:-GBRHosting - Example}" -port 30000 -world "${WORLD_NAME:-Dedicated}" -datadir /server); ` + `[ -n "${SERVER_PASSWORD}" ] && args+=(-password "${SERVER_PASSWORD}"); [ "${CROSSPLAY}" = "true" ] && args+=(-crossplay); ` + `[ -n "${MAX_PLAYERS}" ] && args+=(-maxplayers "${MAX_PLAYERS}"); ./example_server "${args[@]}"`} docs := "Launch with `-name`, `-port`, `-world`, `-password`, `-datadir ` and `-crossplay` to allow consoles." got := undocumentedOptions(game, docs, nil) if len(got) != 1 || !strings.Contains(got[0], "passes -maxplayers,") { t.Fatalf("want only -maxplayers flagged, got %v", got) } // Once removed, nothing is left to flag game["defaultStartupCommand"] = strings.Replace(game["defaultStartupCommand"].(string), `[ -n "${MAX_PLAYERS}" ] && args+=(-maxplayers "${MAX_PLAYERS}"); `, "", 1) if got := undocumentedOptions(game, docs, nil); len(got) != 0 { t.Errorf("got %v", got) } } func TestUndocumentedOptionsIgnoresShellAndUsesImageScripts(t *testing.T) { game := map[string]any{"defaultStartupCommand": `mkdir -p /server/.config; cp -f /a /b; [ -n "$X" ] && [ "$Y" -eq 1 ] && ` + `exec ./server --listen-port=7777 -ServerPlatform=ALL -logs`} facts := &imageFacts{Scripts: map[string]string{"/start.sh": `exec "$@" -logs`}} got := strings.Join(undocumentedOptions(game, "Use --listen-port and -ServerPlatform=ALL for crossplay.", facts), "\n") if got != "" { t.Errorf("shell options, documented ones and ones the image's script uses must pass, got %q", got) } // Without sources the check is off (draft run by hand, or no reader configured) game["defaultStartupCommand"] = "./server -madeup" if p := undocumentedOptions(game, "", nil); p != nil { t.Errorf("got %v", p) } if p := undocumentedOptions(game, "unrelated page", nil); len(p) != 1 { t.Errorf("want -madeup flagged, got %v", p) } } func TestUndocumentedOptionsWholeWords(t *testing.T) { // "-port" in the docs doesn't document "-portal", and "-port" isn't documented by "-portal" game := map[string]any{"defaultStartupCommand": "./server -port 1 -portal x"} got := strings.Join(undocumentedOptions(game, "options: -port", nil), "") if !strings.Contains(got, "passes -portal,") || strings.Contains(got, "-port,") { t.Errorf("got %q", got) } }