From 5688b308925fc3e22965e6da926f0346da9ee15c Mon Sep 17 00:00:00 2001 From: Jeremy McClure Date: Fri, 21 Aug 2026 12:49:27 -0400 Subject: [PATCH] fix: migrate PaperMC to v3 API, fix NeoForge beta fallback, add test suite - Migrate Paper loader from sunset v2 API to v3 (fill.papermc.io) - Fix NeoForge: fall back to beta versions when no stable exists (set -e + grep) - Fix setEula/setperms ordering (was writing eula.txt before /data was writable) - Fix setEula/serverInfoWrite to use absolute paths (/data/server/) - Add VOLUME directive removal (was shadowing bind mounts) - Add comprehensive API test suite (110 tests across 6 suites) - Move test.sh to tests/integration.sh, switch to named volumes - Add Docker build cache to CI workflows - Add .editorconfig, expand .gitignore, improve README docs --- .editorconfig | 22 +++ .gitea/workflows/build-dev.yml | 2 + .gitea/workflows/release.yaml | 4 +- .gitignore | 18 +- Dockerfile | 11 +- README.md | 173 ++++++++++++++++++-- compose.yaml | 17 +- scripts/init | 66 ++++++-- scripts/lib/common.sh | 62 ++++--- scripts/lib/java.sh | 27 ++- scripts/lib/loaders/bedrock.sh | 20 ++- scripts/lib/loaders/fabric.sh | 39 +++-- scripts/lib/loaders/forge.sh | 67 ++++---- scripts/lib/loaders/neoforge.sh | 71 ++++---- scripts/lib/loaders/paper.sh | 66 ++++++-- scripts/lib/loaders/vanilla.sh | 43 ++++- scripts/lib/server.sh | 131 +++++++-------- test.sh => tests/integration.sh | 48 +++--- tests/run_tests.sh | 174 ++++++++++++++++++++ tests/test_bedrock.sh | 252 ++++++++++++++++++++++++++++ tests/test_fabric.sh | 205 +++++++++++++++++++++++ tests/test_forge.sh | 236 +++++++++++++++++++++++++++ tests/test_mojang.sh | 243 +++++++++++++++++++++++++++ tests/test_neoforge.sh | 244 +++++++++++++++++++++++++++ tests/test_paper.sh | 271 ++++++++++++++++++++++++++++++ tests/test_utils.sh | 281 ++++++++++++++++++++++++++++++++ 26 files changed, 2530 insertions(+), 263 deletions(-) create mode 100644 .editorconfig rename test.sh => tests/integration.sh (72%) create mode 100755 tests/run_tests.sh create mode 100755 tests/test_bedrock.sh create mode 100755 tests/test_fabric.sh create mode 100755 tests/test_forge.sh create mode 100755 tests/test_mojang.sh create mode 100755 tests/test_neoforge.sh create mode 100755 tests/test_paper.sh create mode 100755 tests/test_utils.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9bca76e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{sh,bash}] +indent_style = space +indent_size = 2 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[Dockerfile] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitea/workflows/build-dev.yml b/.gitea/workflows/build-dev.yml index 8e2d9b8..37ab566 100644 --- a/.gitea/workflows/build-dev.yml +++ b/.gitea/workflows/build-dev.yml @@ -23,3 +23,5 @@ jobs: context: . push: true tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:dev + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 7cf0d34..0f25bb0 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -24,4 +24,6 @@ jobs: with: context: . push: true - tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:latest \ No newline at end of file + tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index adbb97d..353623b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,17 @@ -data/ \ No newline at end of file +# Runtime data +data/ + +# Editor/IDE +*.swp +*.swo +*~ +.vscode/ +.idea/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# Test artifacts +*.log diff --git a/Dockerfile b/Dockerfile index 62a0ebe..83410b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,10 @@ FROM debian:bookworm-slim ARG UID=1000 ARG GID=1000 +LABEL org.opencontainers.image.title="Dockercraft" \ + org.opencontainers.image.description="Docker image for running Minecraft servers with support for multiple loaders" \ + org.opencontainers.image.source="https://github.com/jeremymcclure/dockercraft" + ENV DEBIAN_FRONTEND=noninteractive SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -17,7 +21,7 @@ RUN apt-get update && \ tini \ unzip \ netcat-openbsd && \ - apt-get autoremove && apt-get clean && \ + apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN groupadd -g ${GID} minecraft && \ @@ -26,15 +30,18 @@ RUN groupadd -g ${GID} minecraft && \ mkdir -p /data && \ chown -R ${UID}:${GID} /data +# Install Jabba (Java version manager) as minecraft user RUN su -c "curl -sL https://github.com/Jabba-Team/jabba/raw/main/install.sh | bash && . ~/.jabba/jabba.sh" minecraft COPY ./scripts/ /scripts/ RUN chmod +x /scripts/init +EXPOSE 25565/tcp 19132/udp 19133/udp +STOPSIGNAL SIGTERM + USER minecraft HEALTHCHECK --start-period=2m --interval=30s --timeout=10s --retries=3 \ CMD bash -c 'if [ "${MC_LOADER}" = "bedrock" ]; then nc -zu 127.0.0.1 19132; else nc -z 127.0.0.1 25565; fi' ENTRYPOINT ["tini", "--", "/scripts/init"] - diff --git a/README.md b/README.md index 5593cf9..6a93732 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ A Docker image for running Minecraft servers with support for multiple loaders. -## Usage +## Features + +- **Multi-loader support** — Vanilla, Paper, Fabric, Forge, NeoForge, and Bedrock +- **Auto-detection** — Java version and loader versions are auto-detected from Mojang manifests +- **Persistent data** — Server files, worlds, and Java installations are stored in `/data` +- **Health checks** — Built-in container health monitoring +- **Flexible Java** — Pin or auto-detect Java version (Temurin, Zulu, GraalVM, etc.) +- **Bedrock support** — Native Bedrock Dedicated Server without Java + +## Quick Start ```yaml services: @@ -33,13 +42,13 @@ services: | Variable | Default | Description | |---|---|---| | `EULA` | — | **Required.** Set to `true` to accept the Minecraft EULA | -| `MC_VERSION` | — | Minecraft version (e.g., `1.21.1`) | -| `MC_LOADER` | — | Server loader: `vanilla`, `paper`, `fabric`, `forge`, or `neoforge` | +| `MC_VERSION` | — | Minecraft version (e.g., `1.21.1`). Required for Java loaders; auto-detected for Bedrock | +| `MC_LOADER` | — | **Required.** Server loader: `vanilla`, `paper`, `fabric`, `forge`, `neoforge`, or `bedrock` | | `MC_LOADER_VERSION` | — | Fallback version for any loader | | `FABRIC_LOADER_VERSION` | `$MC_LOADER_VERSION` | Fabric loader version | | `FORGE_VERSION` | `$MC_LOADER_VERSION` | Forge version (e.g., `52.1.14`) | | `NEOFORGE_VERSION` | `$MC_LOADER_VERSION` | NeoForge version (e.g., `21.1.185`) | -| `PAPER_BUILD` | latest | Paper build number (auto-detects latest if unset) | +| `PAPER_BUILD` | latest | Paper build number (auto-detects latest stable if unset) | | `JAVA_VERSION` | auto-detected | Java version (via Jabba, e.g., `temurin@21`, `zulu@17`). Auto-detects from Mojang manifest if unset: 1.20.5+→21, 1.17-1.20.4→17, 1.17→16, pre-1.17→8 | | `JAR` | `server.jar` | Server jar filename | | `XMS` | `2G` | Initial Java heap size | @@ -50,13 +59,157 @@ services: ## Supported Loaders -- **Vanilla** — Official Mojang server -- **Paper** — High-performance papermc.io -- **Fabric** — Lightweight mod loader via fabricmc.net -- **Forge** — Heavyweight mod loader via minecraftforge.net -- **NeoForge** — Fork of Forge via neoforged.net -- **Bedrock** — Official Bedrock dedicated server (native binary, no Java required) +| Loader | Description | Ports | +|---|---|---| +| **Vanilla** | Official Mojang server | TCP 25565 | +| **Paper** | High-performance fork from papermc.io | TCP 25565 | +| **Fabric** | Lightweight mod loader via fabricmc.net | TCP 25565 | +| **Forge** | Mod loader via minecraftforge.net | TCP 25565 | +| **NeoForge** | Community fork of Forge via neoforged.net | TCP 25565 | +| **Bedrock** | Official Bedrock Dedicated Server (no Java) | UDP 19132 | ## Volumes - `/data` — Persists server data, worlds, config, and Java installations + +## Loader-Specific Examples + +### Paper + +```yaml +environment: + - EULA=true + - MC_VERSION=1.21.1 + - MC_LOADER=paper + - PAPER_BUILD=132 # optional, auto-detects latest stable +``` + +### Fabric + +```yaml +environment: + - EULA=true + - MC_VERSION=1.21.1 + - MC_LOADER=fabric + - FABRIC_LOADER_VERSION=0.19.3 # optional, auto-detects latest +``` + +### Forge / NeoForge + +```yaml +environment: + - EULA=true + - MC_VERSION=1.21.1 + - MC_LOADER=forge # or neoforge + - FORGE_VERSION=52.1.14 # optional, auto-detects latest +``` + +### Bedrock + +```yaml +environment: + - EULA=true + - MC_LOADER=bedrock + - MC_VERSION=1.21.50 # Bedrock server version +ports: + - 19132:19132/udp + - 19133:19133/udp # IPv6 (optional) +``` + +## Java Version Management + +Java is managed via [Jabba](https://github.com/Jabba-Team/jabba). Java installations are cached in `/data/java/` so they persist across container restarts. + +Supported distributions include `temurin`, `zulu`, `adopt`, `openjdk`, `graalvm`, and more. Use the format `distribution@version`: + +```yaml +- JAVA_VERSION=temurin@21 +- JAVA_VERSION=zulu@17 +- JAVA_VERSION=graalvm@21 +``` + +If unset, the Java version is auto-detected from the Mojang version manifest based on `MC_VERSION`. + +## Health Checks + +The container includes a built-in health check: + +- **Java servers** — TCP check on port 25565 +- **Bedrock servers** — UDP check on port 19132 + +The health check has a 2-minute start period to allow the server time to initialize. + +## Troubleshooting + +### Server fails to start + +Check the container logs: + +```bash +docker logs minecraft +``` + +### EULA not accepted + +If you see an EULA error, make sure `EULA=true` is set in your environment variables. + +### Java not found + +If Java installation fails, check that the `/data` volume is writable. Java is installed to `/data/java/` on first run. + +### Wrong UID/GID + +If you get permission errors on server files, check that `PUID` and `PGID` match your host user: + +```bash +id -u # your UID +id -g # your GID +``` + +### Port conflicts + +Make sure the Minecraft port (25565 for Java, 19132 for Bedrock) is not already in use: + +```bash +ss -tlnp | grep 25565 +``` + +## Development + +### Building locally + +```bash +docker build -t dockercraft:local . +``` + +### Running tests + +**API tests** — verify all external APIs are healthy and returning correct data (no Docker needed): + +```bash +# All suites +./tests/run_tests.sh + +# Specific MC version +./tests/run_tests.sh -m 1.21.1 + +# Specific suites only +./tests/run_tests.sh -s mojang,fabric,neoforge +``` + +**Integration tests** — spin up Docker containers and verify full download → install → server start: + +```bash +# Test all loaders +./tests/integration.sh + +# Test a specific loader +./tests/integration.sh -m 1.21.1 -l fabric + +# Custom timeout +./tests/integration.sh -t 300 +``` + +## License + +See [LICENSE](LICENSE) for details. diff --git a/compose.yaml b/compose.yaml index 812b4d5..e31215a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,9 +16,9 @@ services: # --- Loader versions ----------------------------------------------- # Set the var matching your MC_LOADER above. Unset vars are ignored # for other loaders. Leave empty to auto-detect latest stable version. - - FABRIC_LOADER_VERSION=0.19.3 + - FABRIC_LOADER_VERSION= - FORGE_VERSION= - - NEOFORGE_VERSION=21.1.185 + - NEOFORGE_VERSION= - PAPER_BUILD= # # MC_LOADER_VERSION is a fallback for any loader that doesn't have @@ -58,6 +58,19 @@ services: - 25565:25565 #- 19132:19132/udp #- 19133:19133/udp + healthcheck: + test: > + bash -c ' + if [ "${MC_LOADER}" = "bedrock" ]; then + nc -zu 127.0.0.1 19132; + else + nc -z 127.0.0.1 25565; + fi + ' + start_period: 2m + interval: 30s + timeout: 10s + retries: 3 deploy: resources: limits: diff --git a/scripts/init b/scripts/init index 4c77fb6..f8a7732 100755 --- a/scripts/init +++ b/scripts/init @@ -1,49 +1,81 @@ #!/usr/bin/env bash set -euo pipefail -MC_VERSION=${MC_VERSION:-} +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MC_LOADER=${MC_LOADER:-} -MC_LOADER_VERSION=${MC_LOADER_VERSION:-} +# ---------- source helpers first (so log/die are available) ---------------- +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/lib/common.sh" -FABRIC_LOADER_VERSION=${FABRIC_LOADER_VERSION:-${MC_LOADER_VERSION:-}} -FORGE_VERSION=${FORGE_VERSION:-${MC_LOADER_VERSION:-}} -NEOFORGE_VERSION=${NEOFORGE_VERSION:-${MC_LOADER_VERSION:-}} -PAPER_BUILD=${PAPER_BUILD:-""} +# ---------- environment variables ------------------------------------------ -JAVA_VERSION=${JAVA_VERSION:-} -JAVA_HOME=/data/java/${JAVA_VERSION} -JAR=${JAR:-"server.jar"} +MC_VERSION="${MC_VERSION:-}" +MC_LOADER="${MC_LOADER:-}" +MC_LOADER_VERSION="${MC_LOADER_VERSION:-}" -XMS=${XMS:-2G} -XMX=${XMX:-4G} +FABRIC_LOADER_VERSION="${FABRIC_LOADER_VERSION:-${MC_LOADER_VERSION:-}}" +FORGE_VERSION="${FORGE_VERSION:-${MC_LOADER_VERSION:-}}" +NEOFORGE_VERSION="${NEOFORGE_VERSION:-${MC_LOADER_VERSION:-}}" +PAPER_BUILD="${PAPER_BUILD:-}" -ADD_ARGS=${ADD_ARGS:-""} +JAVA_VERSION="${JAVA_VERSION:-}" +JAVA_HOME="/data/java/${JAVA_VERSION}" +JAR="${JAR:-server.jar}" + +XMS="${XMS:-2G}" +XMX="${XMX:-4G}" + +ADD_ARGS="${ADD_ARGS:-}" PUID="${PUID:-1000}" PGID="${PGID:-1000}" -PATH=${JAVA_HOME}/bin:$PATH +PATH="${JAVA_HOME}/bin:$PATH" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# ---------- validate required vars early ----------------------------------- -source "${SCRIPT_DIR}/lib/common.sh" +if [[ -z "$MC_LOADER" ]]; then + die "MC_LOADER is required. Valid: vanilla, paper, fabric, forge, neoforge, bedrock" +fi + +if [[ "$MC_LOADER" != "bedrock" ]]; then + if [[ -z "$MC_VERSION" ]]; then + die "MC_VERSION is required for ${MC_LOADER} loader" + fi +fi + +# ---------- source libraries ------------------------------------------------ + +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/java.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/server.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/vanilla.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/paper.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/fabric.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/forge.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/neoforge.sh" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib/loaders/bedrock.sh" +# ---------- main flow ------------------------------------------------------- + setperms +setEula + if [[ "$MC_LOADER" != "bedrock" ]]; then javaCheck fi + serverSelect + if [[ "$MC_LOADER" != "bedrock" ]]; then buildCommand fi -setEula + serverStart diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 930054f..60e127a 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -1,34 +1,48 @@ -crash() { - sleep 5 - echo "Exit Complete" +# ---------- helpers -------------------------------------------------------- + +log() { + echo -e "[dockercraft] $*" +} + +warn() { + echo -e "[dockercraft] WARNING: $*" >&2 +} + +die() { + echo -e "[dockercraft] ERROR: $*" >&2 + sleep 2 exit 1 } -setperms() { - echo -e "|_______________________________________________________|" - echo -e "|##--------------- Setting Permissions ---------------##|" - echo -e "|#| Setting permissions to UID:${PUID} and GID:${PGID}" - sudo usermod -u ${PUID} minecraft - sudo groupmod -g ${PGID} minecraft - sudo chown -R ${PUID}:${PGID} /data - echo -e "|#|---------------------------------------------------|#|" - sleep 2 - echo -e "|#| DONE" - echo -e "|##---------------------------------------------------##|" - sleep 2 +crash() { + die "$@" } +# ---------- permission setup ----------------------------------------------- + +setperms() { + log "--- Setting permissions to UID:${PUID} GID:${PGID} ---" + sudo usermod -u "${PUID}" minecraft + sudo groupmod -g "${PGID}" minecraft + sudo chown -R "${PUID}:${PGID}" /data + log "Permissions set." +} + +# ---------- EULA ----------------------------------------------------------- + setEula() { if [[ "${EULA:-false}" != "true" ]]; then - echo "EULA not accepted. Set EULA=true to accept the Minecraft EULA (https://aka.ms/MinecraftEULA)" - crash + die "EULA not accepted. Set EULA=true to accept the Minecraft EULA (https://aka.ms/MinecraftEULA)" fi - cat < eula.txt + mkdir -p /data/server + cat <<'EOF' > /data/server/eula.txt #By changing the setting below to TRUE you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA). eula=true EOF } +# ---------- misc ----------------------------------------------------------- + newForge() { if [[ "$(printf '%s\n' "1.17.1" "$MC_VERSION" | sort -V | head -n1)" = "1.17.1" ]]; then NEW_FORGE=true @@ -38,11 +52,11 @@ newForge() { } serverInfoWrite() { - cat < server.info -MC_VERSION=$MC_VERSION + cat < /data/server/server.info +MC_VERSION=${MC_VERSION} -MC_LOADER=$MC_LOADER -MC_LOADER_VERSION=$MC_LOADER_VERSION +MC_LOADER=${MC_LOADER} +MC_LOADER_VERSION=${MC_LOADER_VERSION} JAVA_VERSION=${JAVA_VERSION:-"N/A"} JAR=${JAR:-"N/A"} @@ -51,7 +65,7 @@ XMS=${XMS:-"N/A"} XMX=${XMX:-"N/A"} ADD_ARGS=${ADD_ARGS:-"NONE"} -PUID=$PUID -PGID=$PGID +PUID=${PUID} +PGID=${PGID} EOF } diff --git a/scripts/lib/java.sh b/scripts/lib/java.sh index eb775e3..d28465e 100644 --- a/scripts/lib/java.sh +++ b/scripts/lib/java.sh @@ -3,9 +3,13 @@ javaVersionDetect() { return fi + log "Auto-detecting Java version from Mojang manifest..." + local MANIFEST - MANIFEST=$(curl -sf "https://launchermeta.mojang.com/mc/game/version_manifest.json") + MANIFEST=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://launchermeta.mojang.com/mc/game/version_manifest.json") || true if [[ -z "$MANIFEST" ]]; then + warn "Could not fetch Mojang manifest, defaulting to temurin@21" JAVA_VERSION="temurin@21" return fi @@ -13,13 +17,15 @@ javaVersionDetect() { local VERSION_URL VERSION_URL=$(echo "$MANIFEST" | jq -r ".versions[] | select(.id == \"${MC_VERSION}\") | .url") if [[ -z "$VERSION_URL" || "$VERSION_URL" == "null" ]]; then + warn "Version ${MC_VERSION} not found in manifest, defaulting to temurin@21" JAVA_VERSION="temurin@21" return fi local VERSION_DATA - VERSION_DATA=$(curl -sf "$VERSION_URL") + VERSION_DATA=$(curl -sf --connect-timeout 10 --max-time 30 "$VERSION_URL") || true if [[ -z "$VERSION_DATA" ]]; then + warn "Could not fetch version data for ${MC_VERSION}, defaulting to temurin@21" JAVA_VERSION="temurin@21" return fi @@ -31,6 +37,7 @@ javaVersionDetect() { else JAVA_VERSION="temurin@${JAVA_MAJOR}" fi + log "Detected Java version: ${JAVA_VERSION}" } javaCheck() { @@ -38,16 +45,20 @@ javaCheck() { JAVA_HOME="/data/java/${JAVA_VERSION}" if [[ -f "$HOME/.jabba/jabba.sh" ]]; then - source $HOME/.jabba/jabba.sh + # shellcheck disable=SC1090 + source "$HOME/.jabba/jabba.sh" else - echo "Something went wrong with locating jabba(the java installer)" - exit 1 + die "Jabba not found at ~/.jabba/jabba.sh — installation may have failed" fi if [[ ! -f "/data/java/${JAVA_VERSION}/bin/java" ]]; then - rm -rf /data/java/${JAVA_VERSION} - jabba install ${JAVA_VERSION} -o /data/java/${JAVA_VERSION} + log "Installing Java ${JAVA_VERSION}..." + rm -rf "/data/java/${JAVA_VERSION}" + jabba install "${JAVA_VERSION}" -o "/data/java/${JAVA_VERSION}" + log "Java ${JAVA_VERSION} installed." + else + log "Java ${JAVA_VERSION} already cached." fi - PATH=${JAVA_HOME}/bin:$PATH + PATH="${JAVA_HOME}/bin:$PATH" JAVA="java" } diff --git a/scripts/lib/loaders/bedrock.sh b/scripts/lib/loaders/bedrock.sh index 79b9f40..4a4b934 100644 --- a/scripts/lib/loaders/bedrock.sh +++ b/scripts/lib/loaders/bedrock.sh @@ -5,29 +5,37 @@ _bedrockDownload() { local platform="$1" local version="${2:-}" + log "Fetching Bedrock download URL (platform: ${platform})..." local DL_URL - DL_URL=$(curl -sL -A "Mozilla/5.0 (X11; Linux x86_64)" "$BEDROCK_API_URL" | jq -r '.result.links[] | select(.downloadType == "'"$platform"'") | .downloadUrl // empty') + DL_URL=$(curl -sL -A "Mozilla/5.0 (X11; Linux x86_64)" --connect-timeout 10 --max-time 30 \ + "$BEDROCK_API_URL" | + jq -r ".result.links[] | select(.downloadType == \"${platform}\") | .downloadUrl // empty") + if [[ -z "$DL_URL" ]]; then - DL_URL=$(curl -sL -A "Mozilla/5.0 (X11; Linux x86_64)" "$BEDROCK_FALLBACK_URL" | + warn "Primary API failed, trying fallback source..." + DL_URL=$(curl -sL -A "Mozilla/5.0 (X11; Linux x86_64)" --connect-timeout 10 --max-time 30 \ + "$BEDROCK_FALLBACK_URL" | jq --arg type "$( [[ "$platform" == *Preview* ]] && echo preview || echo release )" -r ' .[$type] | to_entries | sort_by(.key | split(".") | map(tonumber)) | last | .value.linux.url // empty ') fi + if [[ -z "$DL_URL" ]]; then - echo "Failed to look up Bedrock download URL" - crash + die "Failed to look up Bedrock download URL" fi if [[ -n "$version" ]]; then DL_URL=$(echo "$DL_URL" | sed -E "s/(bedrock-server-)[^/]+(\.zip)/\1${version}\2/") fi + local VER VER=$(echo "$DL_URL" | grep -oP 'bedrock-server-\K[\d.]+(?=\.zip)') - echo "$DL_URL" - curl -o bedrock-server.zip -L -A "Mozilla/5.0 (X11; Linux x86_64)" "$DL_URL" + log "Downloading Bedrock server version ${VER}..." + curl -fL -o bedrock-server.zip -A "Mozilla/5.0 (X11; Linux x86_64)" "$DL_URL" unzip -o bedrock-server.zip rm -f bedrock-server.zip chmod +x bedrock_server + log "Bedrock ${VER} downloaded." } getBedrock() { diff --git a/scripts/lib/loaders/fabric.sh b/scripts/lib/loaders/fabric.sh index 203c74b..bbb9b54 100644 --- a/scripts/lib/loaders/fabric.sh +++ b/scripts/lib/loaders/fabric.sh @@ -1,22 +1,33 @@ getFabric() { SERVER_TYPE="Fabric Minecraft" - LATEST_BUILD=$(curl -s https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION} | jq -r '.[0].loader.version') - LATEST_INSTALLER=$(curl -s https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml | xmllint --xpath "/metadata/versioning/latest/text()" -) - if [[ -z "$MC_VERSION" ]]; then - echo "Minecraft version not set" - exit 1 + if [[ -z "${MC_VERSION}" ]]; then + die "MC_VERSION is required for Fabric loader" fi - VER=$MC_VERSION + log "Fetching Fabric loader version for MC ${MC_VERSION}..." + local LATEST_BUILD LATEST_INSTALLER + LATEST_BUILD=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}" | + jq -r '.[0].loader.version') + + LATEST_INSTALLER=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml" | + xmllint --xpath "/metadata/versioning/latest/text()" -) + + if [[ -z "$LATEST_BUILD" || "$LATEST_BUILD" == "null" ]]; then + die "No Fabric loader found for MC ${MC_VERSION}" + fi + if [[ -z "$LATEST_INSTALLER" ]]; then + die "Could not determine latest Fabric installer version" + fi + + local VER_LOADER VER_LOADER="${FABRIC_LOADER_VERSION:-${LATEST_BUILD}}" - DL_URL=https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${VER_LOADER}/${LATEST_INSTALLER}/server/jar + local DL_URL + DL_URL="https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${VER_LOADER}/${LATEST_INSTALLER}/server/jar" - if [ "$LATEST_BUILD" != "null" ]; then - echo "$DL_URL" - curl -o server.jar "${DL_URL}" - else - echo "No valid Fabric build found for version ${MC_VERSION}" - crash - fi + log "Downloading Fabric (loader ${VER_LOADER}, installer ${LATEST_INSTALLER})..." + curl -fL -o server.jar "${DL_URL}" + log "Fabric ${MC_VERSION} (loader ${VER_LOADER}) downloaded." } diff --git a/scripts/lib/loaders/forge.sh b/scripts/lib/loaders/forge.sh index 34fc4bf..5d0d609 100644 --- a/scripts/lib/loaders/forge.sh +++ b/scripts/lib/loaders/forge.sh @@ -1,42 +1,45 @@ getForge() { SERVER_TYPE="Forge Server" - if [[ -n "$MC_VERSION" ]]; then - VER_MINECRAFT=$MC_VERSION - if [[ -n "${FORGE_VERSION}" ]]; then - VER_LOADER="${FORGE_VERSION}" - DL_URL=https://maven.minecraftforge.net/net/minecraftforge/forge/${VER_MINECRAFT}-${VER_LOADER}/forge-${VER_MINECRAFT}-${VER_LOADER}-installer.jar - else - LATEST_BUILD=$(curl -s https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json | jq -r ".promos[\"${VER_MINECRAFT}-latest\"]") - VER_LOADER=$LATEST_BUILD - DL_URL=https://maven.minecraftforge.net/net/minecraftforge/forge/${VER_MINECRAFT}-${VER_LOADER}/forge-${VER_MINECRAFT}-${VER_LOADER}-installer.jar - fi - else - echo "Minecraft version not set" - exit 1 + if [[ -z "${MC_VERSION}" ]]; then + die "MC_VERSION is required for Forge loader" fi - if [[ -n "$DL_URL" ]]; then - echo "$DL_URL" - rm -rf server.jar - curl -o forge-install.jar "${DL_URL}" - echo "$JAVA -jar forge-install.jar --installServer" - $JAVA -jar forge-install.jar --installServer + local DL_URL + if [[ -n "${FORGE_VERSION}" ]]; then + VER_LOADER="${FORGE_VERSION}" + DL_URL="https://maven.minecraftforge.net/net/minecraftforge/forge/${MC_VERSION}-${VER_LOADER}/forge-${MC_VERSION}-${VER_LOADER}-installer.jar" + else + log "Fetching latest Forge version for MC ${MC_VERSION}..." + local LATEST_BUILD + LATEST_BUILD=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json" | + jq -r ".promos[\"${MC_VERSION}-latest\"]") - if [[ "$(printf '%s\n' "1.17.1" "$MC_VERSION" | sort -V | head -n1)" = "1.17.1" ]]; then - NEW_FORGE=true - rm -f forge-install.jar - rm -f forge-install.jar.log - else - NEW_FORGE=false - rm -f forge-install.jar - rm -f forge-install.jar.log - ln -s forge-"${VER_MINECRAFT}"-"${VER_LOADER}".jar server.jar + if [[ -z "$LATEST_BUILD" || "$LATEST_BUILD" == "null" ]]; then + die "No Forge version found for MC ${MC_VERSION}" fi - touch server.properties - else - echo "No Valid Download URL" - crash + VER_LOADER="$LATEST_BUILD" + DL_URL="https://maven.minecraftforge.net/net/minecraftforge/forge/${MC_VERSION}-${VER_LOADER}/forge-${MC_VERSION}-${VER_LOADER}-installer.jar" fi + + log "Downloading Forge ${MC_VERSION}-${VER_LOADER} installer..." + rm -f server.jar + curl -fL -o forge-install.jar "${DL_URL}" + + log "Running Forge installer..." + $JAVA -jar forge-install.jar --installServer + + newForge + rm -f forge-install.jar forge-install.jar.log + + if [[ "${NEW_FORGE}" == "true" ]]; then + log "Forge ${MC_VERSION} installed (new-style launcher)." + else + ln -sf "forge-${MC_VERSION}-${VER_LOADER}.jar" server.jar + log "Forge ${MC_VERSION}-${VER_LOADER} installed." + fi + + touch server.properties } diff --git a/scripts/lib/loaders/neoforge.sh b/scripts/lib/loaders/neoforge.sh index de70f61..993ccfb 100644 --- a/scripts/lib/loaders/neoforge.sh +++ b/scripts/lib/loaders/neoforge.sh @@ -1,40 +1,55 @@ getNeoForge() { SERVER_TYPE="NeoForge Server" - if [[ -n "$MC_VERSION" ]]; then - VER_MINECRAFT=$MC_VERSION - if [[ -n "${NEOFORGE_VERSION}" ]]; then - VER_LOADER="${NEOFORGE_VERSION}" - else - MC_MINOR="${MC_VERSION#1.}" - VER_LOADER=$(curl -s https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml | \ - xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null | \ - tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep "^${MC_MINOR}\." | sort -V | tail -1) - if [[ -z "$VER_LOADER" ]]; then - echo "Could not auto-detect NeoForge version for MC ${MC_VERSION}" - crash + if [[ -z "${MC_VERSION}" ]]; then + die "MC_VERSION is required for NeoForge loader" + fi + + local VER_MINECRAFT DL_URL + VER_MINECRAFT="${MC_VERSION}" + + if [[ -n "${NEOFORGE_VERSION}" ]]; then + VER_LOADER="${NEOFORGE_VERSION}" + else + log "Auto-detecting latest NeoForge version for MC ${MC_VERSION}..." + local MC_MINOR ALL_VERSIONS + MC_MINOR="${MC_VERSION#1.}" + ALL_VERSIONS=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml" | + xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null | + tr ' ' '\n' | grep "^${MC_MINOR}\." | sort -V || true) + + # Prefer stable versions, fall back to beta/alpha if none exist + VER_LOADER=$(echo "$ALL_VERSIONS" | grep -v -- '-beta\|-alpha' | tail -1 || true) + if [[ -z "$VER_LOADER" ]]; then + VER_LOADER=$(echo "$ALL_VERSIONS" | tail -1) + if [[ -n "$VER_LOADER" ]]; then + warn "No stable NeoForge version for MC ${MC_VERSION}, using ${VER_LOADER}" fi fi - DL_URL=https://maven.neoforged.net/releases/net/neoforged/neoforge/$VER_LOADER/neoforge-$VER_LOADER-installer.jar + + if [[ -z "$VER_LOADER" ]]; then + die "Could not auto-detect NeoForge version for MC ${MC_VERSION}" + fi fi - if [[ -n "$DL_URL" ]]; then - echo "$DL_URL" - rm -rf server.jar + DL_URL="https://maven.neoforged.net/releases/net/neoforged/neoforge/${VER_LOADER}/neoforge-${VER_LOADER}-installer.jar" - curl -o "neoforge-${VER_LOADER}-installer.jar" "${DL_URL}" - $JAVA -jar neoforge-$VER_LOADER-installer.jar --installServer --server.jar + log "Downloading NeoForge ${VER_LOADER} installer..." + rm -f server.jar + curl -fL -o "neoforge-${VER_LOADER}-installer.jar" "${DL_URL}" - mv server.jar neoforge-$VER_LOADER.jar - ln -s neoforge-$VER_LOADER.jar server.jar + log "Running NeoForge installer..." + $JAVA -jar "neoforge-${VER_LOADER}-installer.jar" --installServer --server.jar - touch server.properties + mv server.jar "neoforge-${VER_LOADER}.jar" + ln -sf "neoforge-${VER_LOADER}.jar" server.jar - rm -f neoforge-$VER_LOADER-installer.jar - rm -f neoforge-$VER_LOADER-installer.jar.log - rm -f README.txt - rm -f run.bat - else - echo "No Valid Download URL" - fi + rm -f "neoforge-${VER_LOADER}-installer.jar" \ + "neoforge-${VER_LOADER}-installer.jar.log" \ + README.txt \ + run.bat + + touch server.properties + log "NeoForge ${MC_VERSION}-${VER_LOADER} installed." } diff --git a/scripts/lib/loaders/paper.sh b/scripts/lib/loaders/paper.sh index a553f58..9a7d0b2 100644 --- a/scripts/lib/loaders/paper.sh +++ b/scripts/lib/loaders/paper.sh @@ -1,23 +1,59 @@ getPaper() { SERVER_TYPE="Paper Minecraft" - if [[ ! -f server.jar ]]; then - if [[ -n "${PAPER_BUILD}" ]]; then - LATEST_BUILD="${PAPER_BUILD}" - else - LATEST_BUILD=$(curl -s https://api.papermc.io/v2/projects/${MC_LOADER}/versions/${MC_VERSION}/builds | - jq -r '[.builds[] | select(.channel | ascii_upcase == "STABLE")] | .[-1].build') + + if [[ -z "${MC_VERSION}" ]]; then + die "MC_VERSION is required for Paper loader" + fi + + if [[ -f server.jar ]]; then + log "Paper server.jar already exists, skipping download." + return + fi + + local PAPERMC_URL + + if [[ -n "${PAPER_BUILD}" ]]; then + log "Fetching Paper build ${PAPER_BUILD} for MC ${MC_VERSION}..." + local BUILD_DATA + BUILD_DATA=$(curl -sf --connect-timeout 10 --max-time 30 \ + -H "User-Agent: dockercraft/1.0" \ + "https://fill.papermc.io/v3/projects/paper/versions/${MC_VERSION}/builds") || true + + if [[ -n "$BUILD_DATA" ]]; then + PAPERMC_URL=$(echo "$BUILD_DATA" | jq -r \ + "map(select(.id == ${PAPER_BUILD})) | .[0].downloads.\"server:default\".url // empty") fi - if [ "$LATEST_BUILD" != "null" ]; then - JAR_NAME=${MC_LOADER}-${MC_VERSION}-${LATEST_BUILD}.jar - PAPERMC_URL="https://api.papermc.io/v2/projects/${MC_LOADER}/versions/${MC_VERSION}/builds/${LATEST_BUILD}/downloads/${JAR_NAME}" + if [[ -z "$PAPERMC_URL" ]]; then + die "Paper build ${PAPER_BUILD} not found for MC ${MC_VERSION}" + fi + else + log "Querying PaperMC API for latest stable build of ${MC_VERSION}..." + local BUILDS_DATA + BUILDS_DATA=$(curl -sf --connect-timeout 10 --max-time 30 \ + -H "User-Agent: dockercraft/1.0" \ + "https://fill.papermc.io/v3/projects/paper/versions/${MC_VERSION}/builds") || true - curl -o server.jar "$PAPERMC_URL" - echo "Download completed" - echo "Minecraft Version: ${MC_VERSION}" - echo "Paper Version: ${LATEST_BUILD}" - else - echo "No stable build for version $MC_VERSION found :(" + if [[ -z "$BUILDS_DATA" ]]; then + die "Could not fetch Paper builds for MC ${MC_VERSION}" + fi + + # Check for API error + if echo "$BUILDS_DATA" | jq -e '.ok == false' >/dev/null 2>&1; then + local ERR_MSG + ERR_MSG=$(echo "$BUILDS_DATA" | jq -r '.message // "Unknown error"') + die "PaperMC API error: ${ERR_MSG}" + fi + + PAPERMC_URL=$(echo "$BUILDS_DATA" | jq -r \ + 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty') + + if [[ -z "$PAPERMC_URL" ]]; then + die "No stable Paper build found for MC ${MC_VERSION}" fi fi + + log "Downloading Paper for MC ${MC_VERSION}..." + curl -fL -o server.jar "$PAPERMC_URL" + log "Paper for MC ${MC_VERSION} downloaded." } diff --git a/scripts/lib/loaders/vanilla.sh b/scripts/lib/loaders/vanilla.sh index 5e01c71..0bfaada 100644 --- a/scripts/lib/loaders/vanilla.sh +++ b/scripts/lib/loaders/vanilla.sh @@ -1,13 +1,38 @@ getVanilla() { SERVER_TYPE="Vanilla Minecraft" - MANIFEST=$(curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json) - VERSION_URL=$(echo "$MANIFEST" | jq -r ".versions[] | select(.id == \"${MC_VERSION}\") | .url") - if [[ -n "$VERSION_URL" ]] && [[ "$VERSION_URL" != "null" ]]; then - VERSION_DATA=$(curl -s "$VERSION_URL") - DOWNLOAD_URL=$(echo "$VERSION_DATA" | jq -r ".downloads.server.url") - curl -o server.jar "$DOWNLOAD_URL" - else - echo "Version ${MC_VERSION} not found in Mojang manifest" - crash + + if [[ -z "${MC_VERSION}" ]]; then + die "MC_VERSION is required for Vanilla loader" fi + + log "Fetching Mojang version manifest..." + local MANIFEST + MANIFEST=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://launchermeta.mojang.com/mc/game/version_manifest.json") || true + + if [[ -z "$MANIFEST" ]]; then + die "Could not fetch Mojang version manifest" + fi + + local VERSION_URL + VERSION_URL=$(echo "$MANIFEST" | jq -r ".versions[] | select(.id == \"${MC_VERSION}\") | .url") + + if [[ -z "$VERSION_URL" || "$VERSION_URL" == "null" ]]; then + die "Version ${MC_VERSION} not found in Mojang manifest" + fi + + local VERSION_DATA DOWNLOAD_URL + VERSION_DATA=$(curl -sf --connect-timeout 10 --max-time 30 "$VERSION_URL") || true + if [[ -z "$VERSION_DATA" ]]; then + die "Could not fetch version data for ${MC_VERSION}" + fi + + DOWNLOAD_URL=$(echo "$VERSION_DATA" | jq -r ".downloads.server.url") + if [[ -z "$DOWNLOAD_URL" || "$DOWNLOAD_URL" == "null" ]]; then + die "No server download URL found for ${MC_VERSION}" + fi + + log "Downloading Vanilla ${MC_VERSION} server..." + curl -fL -o server.jar "$DOWNLOAD_URL" + log "Vanilla ${MC_VERSION} downloaded." } diff --git a/scripts/lib/server.sh b/scripts/lib/server.sh index 08f3315..1dd83fa 100644 --- a/scripts/lib/server.sh +++ b/scripts/lib/server.sh @@ -1,10 +1,10 @@ serverSelect() { mkdir -p /data/server - cd /data/server || exit + cd /data/server || die "Cannot access /data/server" if [[ -f "server.jar" ]] || [[ -f "run.sh" ]] || [[ -f "bedrock_server" ]]; then - echo "Found what appears to be a server, sending it" + log "Found existing server files, skipping download." else - case $MC_LOADER in + case "$MC_LOADER" in vanilla) getVanilla ;; @@ -24,8 +24,7 @@ serverSelect() { getBedrock ;; *) - echo "Server of type ${MC_LOADER} is not recognized." - crash + die "Server type '${MC_LOADER}' is not recognized. Valid: vanilla, paper, fabric, forge, neoforge, bedrock" ;; esac fi @@ -33,78 +32,72 @@ serverSelect() { buildCommand() { JAVA_ARGS="-Xms${XMS} -Xmx${XMX}" - if [[ -n $ADD_ARGS ]]; then + if [[ -n "${ADD_ARGS}" ]]; then RUN_STRING="${ADD_ARGS} ${JAVA_ARGS} -jar ${JAR} nogui" else RUN_STRING="${JAVA_ARGS} -jar ${JAR} nogui" fi } -serverStart() { +detectForgeVersion() { + # Detect the forge/neoforge version from the installed jar names VER_LOADER="${VER_LOADER:-}" - if [[ -z "$VER_LOADER" ]]; then - if [[ "$MC_LOADER" == "neoforge" ]] && [[ -L "/data/server/server.jar" ]]; then - VER_LOADER=$(readlink "/data/server/server.jar" | sed 's/^neoforge-//;s/\.jar$//') - elif [[ "$MC_LOADER" == "forge" ]]; then - local forge_jar - forge_jar=$(ls /data/server/forge-${MC_VERSION}-*.jar 2>/dev/null | head -1) - if [[ -n "$forge_jar" ]]; then - VER_LOADER=${forge_jar#/data/server/forge-${MC_VERSION}-} - VER_LOADER=${VER_LOADER%.jar} - fi - fi + if [[ -n "$VER_LOADER" ]]; then + return fi - while true; do - cd /data/server || exit - if [[ ! -f "server.properties" ]]; then touch "server.properties"; fi - - if [[ -f $JAR || -f "run.sh" || -f "bedrock_server" ]]; then - echo -e "|_______________________________________________________|" - echo -e "|##----------------- Server Starting -----------------##|" - echo -e "|#| Minecraft Version: $MC_VERSION" - echo -e "|#| " - echo -e "|#| Loader Type: ${SERVER_TYPE:-$MC_LOADER}" - echo -e "|#| Loader Version: $MC_LOADER_VERSION" - echo -e "|#| " - if [[ "$MC_LOADER" != "bedrock" ]]; then - echo -e "|#| Java Version: $JAVA_VERSION" - echo -e "|#| Jar: $JAR" - echo -e "|#| Memory Allocated: " - echo -e "|#| Xms: $XMS" - echo -e "|#| Xmx: $XMX" - echo -e "|#| Additional Args: ${ADD_ARGS:-"NONE"}" - fi - echo -e "|#| " - echo -e "|#| Running as:" - echo -e "|#| User: $PUID" - echo -e "|#| Group: $PGID" - echo -e "|#| " - if [[ "$MC_LOADER" == "bedrock" ]]; then - echo -e "|#| Run Command:" - echo -e "|#| LD_LIBRARY_PATH=. ./bedrock_server" - elif [[ ${NEW_FORGE:-false} != "true" ]]; then - echo -e "|#| Run Command:" - echo -e "|#| java $RUN_STRING" - fi - echo -e "|##---------------------------------------------------##|" - echo -e "|-------------------------------------------------------|" - echo -e " " - serverInfoWrite - newForge - if [[ "$MC_LOADER" == "bedrock" ]]; then - exec env LD_LIBRARY_PATH=. ./bedrock_server - elif [[ "$MC_LOADER" == "neoforge" ]]; then - exec java @user_jvm_args.txt @libraries/net/neoforged/neoforge/"${VER_LOADER}"/unix_args.txt "$@" - elif [[ "$MC_LOADER" == "forge" ]] && [[ ${NEW_FORGE:-false} == "true" ]]; then - exec java @user_jvm_args.txt @libraries/net/minecraftforge/forge/"$MC_VERSION"-"${VER_LOADER}"/unix_args.txt "$@" - else - echo "java $RUN_STRING" - exec java $RUN_STRING - fi - else - echo "Server Not Found." - crash + if [[ "$MC_LOADER" == "neoforge" ]] && [[ -L "/data/server/server.jar" ]]; then + VER_LOADER=$(readlink "/data/server/server.jar" | sed 's/^neoforge-//;s/\.jar$//') + elif [[ "$MC_LOADER" == "forge" ]]; then + local forge_jar + forge_jar=$(ls /data/server/forge-"${MC_VERSION}"-*.jar 2>/dev/null | head -1) + if [[ -n "$forge_jar" ]]; then + VER_LOADER="${forge_jar#/data/server/forge-${MC_VERSION}-}" + VER_LOADER="${VER_LOADER%.jar}" fi - done + fi +} + +serverStart() { + detectForgeVersion + newForge + + cd /data/server || die "Cannot access /data/server" + if [[ ! -f "server.properties" ]]; then + touch "server.properties" + fi + + if [[ ! -f "$JAR" ]] && [[ ! -f "run.sh" ]] && [[ ! -f "bedrock_server" ]]; then + die "Server binary not found in /data/server" + fi + + log "--- Server Starting ---" + log "Minecraft Version: ${MC_VERSION}" + log "Loader Type: ${SERVER_TYPE:-$MC_LOADER}" + log "Loader Version: ${MC_LOADER_VERSION:-auto}" + if [[ "$MC_LOADER" != "bedrock" ]]; then + log "Java Version: ${JAVA_VERSION}" + log "Jar: ${JAR}" + log "Memory: Xms=${XMS} Xmx=${XMX}" + if [[ -n "${ADD_ARGS}" ]]; then + log "Additional Args: ${ADD_ARGS}" + fi + fi + log "Running as UID:${PUID} GID:${PGID}" + + serverInfoWrite + + if [[ "$MC_LOADER" == "bedrock" ]]; then + log "Starting Bedrock server..." + exec env LD_LIBRARY_PATH=. ./bedrock_server + elif [[ "$MC_LOADER" == "neoforge" ]]; then + log "Starting NeoForge server..." + exec java @user_jvm_args.txt @libraries/net/neoforged/neoforge/"${VER_LOADER}"/unix_args.txt "$@" + elif [[ "$MC_LOADER" == "forge" ]] && [[ "${NEW_FORGE:-false}" == "true" ]]; then + log "Starting Forge server (new-style)..." + exec java @user_jvm_args.txt @libraries/net/minecraftforge/forge/"${MC_VERSION}"-"${VER_LOADER}"/unix_args.txt "$@" + else + log "Starting ${SERVER_TYPE:-$MC_LOADER} server..." + exec java $RUN_STRING + fi } diff --git a/test.sh b/tests/integration.sh similarity index 72% rename from test.sh rename to tests/integration.sh index bcc0209..6872912 100755 --- a/test.sh +++ b/tests/integration.sh @@ -11,6 +11,7 @@ Options: -m, --mc-version Minecraft version (default: auto-detect latest) -l, --loader Loader to test (default: all) -v, --loader-version Loader version (default: auto-detect latest) + -t, --timeout Max wait for server jar (default: 180) -h, --help Show this help Examples: @@ -25,14 +26,19 @@ EOF } detect_mc_version() { - curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json | \ + curl -sf --connect-timeout 10 --max-time 30 \ + "https://launchermeta.mojang.com/mc/game/version_manifest.json" | jq -r '.latest.release' } +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + IMAGE="dockercraft-test:latest" MC_VERSION="" LOADER="" LOADER_VERSION="" +TIMEOUT=180 PASS=0 FAIL=0 @@ -42,6 +48,7 @@ while [[ $# -gt 0 ]]; do -m|--mc-version) MC_VERSION="$2"; shift 2 ;; -l|--loader) LOADER="$2"; shift 2 ;; -v|--loader-version) LOADER_VERSION="$2"; shift 2 ;; + -t|--timeout) TIMEOUT="$2"; shift 2 ;; *) echo "Unknown option: $1"; usage ;; esac done @@ -49,7 +56,7 @@ done if [[ -z "$MC_VERSION" ]]; then echo "Detecting latest Minecraft version..." MC_VERSION=$(detect_mc_version) - echo " → ${MC_VERSION}" + echo " -> ${MC_VERSION}" fi if [[ -z "$LOADER" ]]; then @@ -62,6 +69,7 @@ cleanup() { local name="$1" docker kill "dc-test-${name}" 2>/dev/null || true docker rm "dc-test-${name}" 2>/dev/null || true + docker volume rm "dc-test-vol-${name}" 2>/dev/null || true } run_test() { @@ -71,22 +79,24 @@ run_test() { echo "=== Testing: ${name} ===" cleanup "$name" - local data_dir - data_dir=$(mktemp -d) + # Use a named volume instead of a bind mount (avoids host filesystem quirks) + docker volume create "dc-test-vol-${name}" >/dev/null + local CONTAINER_ID if CONTAINER_ID=$(docker run -d --name "dc-test-${name}" \ -e EULA=true \ -e MC_VERSION="${MC_VERSION}" \ "$@" \ - -v "${data_dir}:/data" \ + -v "dc-test-vol-${name}:/data" \ "${IMAGE}" 2>&1); then local waited=0 local jar_found=false - while [[ $waited -lt 180 ]]; do - if [[ -f "${data_dir}/server/server.jar" ]] || \ - (find "${data_dir}/server" -name 'forge-*-server.jar' 2>/dev/null | grep -q .) || \ - [[ -f "${data_dir}/server/bedrock_server" ]]; then + while [[ $waited -lt $TIMEOUT ]]; do + # Check inside the container for the server binary + if docker exec "dc-test-${name}" test -f /data/server/server.jar 2>/dev/null || \ + docker exec "dc-test-${name}" find /data/server -name 'forge-*-server.jar' 2>/dev/null | grep -q . || \ + docker exec "dc-test-${name}" test -f /data/server/bedrock_server 2>/dev/null; then jar_found=true break fi @@ -97,7 +107,7 @@ run_test() { echo "" if $jar_found; then - if [[ -f "${data_dir}/server/bedrock_server" ]]; then + if docker exec "dc-test-${name}" test -f /data/server/bedrock_server 2>/dev/null; then echo " PASS (bedrock_server found in ~${waited}s)" else echo " PASS (server jar found in ~${waited}s)" @@ -105,9 +115,9 @@ run_test() { PASS=$((PASS + 1)) else echo "" - echo " FAIL: server.jar not found after 180s" + echo " FAIL: server binary not found after ${TIMEOUT}s" echo " --- container logs ---" - docker logs "dc-test-${name}" 2>&1 || true + docker logs "dc-test-${name}" 2>&1 | tail -50 || true echo " ---------------------" FAIL=$((FAIL + 1)) fi @@ -119,22 +129,10 @@ run_test() { fi cleanup "$name" - rm -rf "$data_dir" } echo "Building image..." -docker build -t "${IMAGE}" . - -echo "Pre-warming Java cache..." -WARM_ID=$(docker run -d --name "dc-warm" \ - -e MC_VERSION="${MC_VERSION}" \ - -e MC_LOADER=vanilla \ - -e EULA=true \ - -v "$(mktemp -d):/data" \ - "${IMAGE}" 2>&1) && sleep 5 -docker kill "dc-warm" 2>/dev/null || true -docker rm "dc-warm" 2>/dev/null || true -echo " done" +DOCKER_BUILDKIT=0 docker build -t "${IMAGE}" "${PROJECT_ROOT}" for t in "${tests[@]}"; do case $t in diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..ffca999 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Main test runner for Dockercraft API tests +# Runs all test suites against the external APIs to verify they're healthy +# and returning expected data. +# +# Usage: +# ./tests/run_tests.sh # Run all tests +# ./tests/run_tests.sh -m 1.21.1 # Test with specific MC version +# ./tests/run_tests.sh -s mojang # Run only Mojang tests +# ./tests/run_tests.sh -s paper,fabric # Run Paper and Fabric tests +# ./tests/run_tests.sh --list # List available test suites +# +# Exit codes: +# 0 = all tests passed +# 1 = one or more tests failed + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ---------- configuration -------------------------------------------------- + +MC_VERSION="" +SUITE="" +LIST_ONLY=false + +# Available test suites +declare -A SUITES=( + ["mojang"]="test_mojang.sh" + ["paper"]="test_paper.sh" + ["fabric"]="test_fabric.sh" + ["forge"]="test_forge.sh" + ["neoforge"]="test_neoforge.sh" + ["bedrock"]="test_bedrock.sh" +) + +SUITE_ORDER=("mojang" "paper" "fabric" "forge" "neoforge" "bedrock") + +# ---------- argument parsing ----------------------------------------------- + +usage() { + cat < Minecraft version (default: auto-detect latest) + -s, --suite Run specific suite(s), comma-separated + Valid: mojang, paper, fabric, forge, neoforge, bedrock + -l, --list List available test suites and exit + -h, --help Show this help + +Examples: + $(basename "$0") # All suites, latest MC + $(basename "$0") -m 1.21.1 # All suites, MC 1.21.1 + $(basename "$0") -s mojang # Just Mojang API tests + $(basename "$0") -s paper,fabric,neoforge # Specific suites + $(basename "$0") -l # List suites + +EOF + exit 0 +} + +list_suites() { + echo "Available test suites:" + echo "" + for s in "${SUITE_ORDER[@]}"; do + echo " ${s} - ${SUITES[$s]}" + done + echo "" + echo "Usage: $(basename "$0") -s mojang,paper" + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + -m|--mc-version) MC_VERSION="$2"; shift 2 ;; + -s|--suite) SUITE="$2"; shift 2 ;; + -l|--list) LIST_ONLY=true; shift ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +if $LIST_ONLY; then + list_suites +fi + +# ---------- detect MC version if needed ------------------------------------ + +if [[ -z "$MC_VERSION" ]]; then + echo "Detecting latest Minecraft version..." + MC_VERSION=$(curl -sf --connect-timeout 10 --max-time 30 \ + "https://launchermeta.mojang.com/mc/game/version_manifest.json" | + jq -r '.latest.release') + echo " -> ${MC_VERSION}" +fi + +# ---------- determine which suites to run ---------------------------------- + +if [[ -n "$SUITE" ]]; then + IFS=',' read -ra RUN_SUITES <<< "$SUITE" +else + RUN_SUITES=("${SUITE_ORDER[@]}") +fi + +# ---------- validate suite names ------------------------------------------- + +for s in "${RUN_SUITES[@]}"; do + if [[ -z "${SUITES[$s]+x}" ]]; then + echo "Unknown suite: ${s}" + echo "Valid suites: ${!SUITES[*]}" + exit 1 + fi +done + +# ---------- run suites ----------------------------------------------------- + +OVERALL_PASS=0 +OVERALL_FAIL=0 +OVERALL_SUITES=0 +FAILED_SUITES=() + +echo "" +echo "========================================" +echo " Dockercraft API Test Runner" +echo " MC Version: ${MC_VERSION}" +echo "========================================" +echo "" + +for s in "${RUN_SUITES[@]}"; do + test_file="${SCRIPT_DIR}/${SUITES[$s]}" + + if [[ ! -f "$test_file" ]]; then + echo "Test file not found: ${test_file}" + OVERALL_FAIL=$((OVERALL_FAIL + 1)) + FAILED_SUITES+=("$s") + continue + fi + + OVERALL_SUITES=$((OVERALL_SUITES + 1)) + + # Run the suite and capture exit code + if bash "$test_file" "$MC_VERSION"; then + OVERALL_PASS=$((OVERALL_PASS + 1)) + else + OVERALL_FAIL=$((OVERALL_FAIL + 1)) + FAILED_SUITES+=("$s") + fi +done + +# ---------- final summary -------------------------------------------------- + +echo "" +echo "========================================" +echo " Overall Results" +echo "========================================" +echo " Suites run: ${OVERALL_SUITES}" +echo " Suites passed: ${OVERALL_PASS}" +echo " Suites failed: ${OVERALL_FAIL}" +echo "" + +if [[ $OVERALL_FAIL -gt 0 ]]; then + echo "Failed suites:" + for s in "${FAILED_SUITES[@]}"; do + echo " - ${s}" + done + echo "" + exit 1 +fi + +echo "All suites passed!" +exit 0 diff --git a/tests/test_bedrock.sh b/tests/test_bedrock.sh new file mode 100755 index 0000000..1d21530 --- /dev/null +++ b/tests/test_bedrock.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# Tests for Bedrock Dedicated Server download API +# Covers: Primary API, fallback API, version parsing +# Usage: ./tests/test_bedrock.sh + +source "$(dirname "$0")/test_utils.sh" + +echo -e "\n${BOLD}Bedrock API Tests${NC}" +echo "" + +BEDROCK_API_URL="https://net.web.minecraft-services.net/api/v1.0/download/links" +BEDROCK_FALLBACK_URL="https://raw.githubusercontent.com/kittizz/bedrock-server-downloads/refs/heads/main/bedrock-server-downloads.json" + +# ---------- Primary API (minecraft-services.net) --------------------------- + +test_bedrock_primary_api_reachable() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + assert_not_empty "$body" "Bedrock primary API body should not be empty" +} + +test_bedrock_primary_api_is_json() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + assert_json_valid "$body" "Primary API response should be valid JSON" +} + +test_bedrock_primary_api_has_result() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + assert_json_field "$body" '.result' "Should have .result field" +} + +test_bedrock_primary_api_has_links() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + assert_json_field "$body" '.result.links' "Should have .result.links array" +} + +test_bedrock_primary_api_links_is_array() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local type + type=$(echo "$body" | jq -r '.result.links | type') + assert_equals "$type" "array" ".result.links should be an array" +} + +test_bedrock_primary_has_linux_entry() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local found + found=$(echo "$body" | jq -r '.result.links[] | select(.downloadType == "serverBedrockLinux") | .downloadUrl // empty') + + if [[ -n "$found" && "$found" != "null" ]]; then + return 0 + else + echo "ASSERT FAILED: No serverBedrockLinux entry found" + return 1 + fi +} + +test_bedrock_primary_linux_url_format() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local url + url=$(echo "$body" | jq -r '.result.links[] | select(.downloadType == "serverBedrockLinux") | .downloadUrl // empty') + + assert_matches "$url" "bedrock-server-.*\.zip$" \ + "Linux download URL should end with bedrock-server-{version}.zip" +} + +test_bedrock_primary_linux_url_reachable() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local url + url=$(echo "$body" | jq -r '.result.links[] | select(.downloadType == "serverBedrockLinux") | .downloadUrl // empty') + + if [[ -z "$url" ]]; then + echo "No Linux download URL found" + return 1 + fi + + # Just check HTTP HEAD to avoid downloading the full zip + local http_code + http_code=$(curl -sL -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 \ + -A "Mozilla/5.0 (X11; Linux x86_64)" -I "$url") || true + assert_equals "$http_code" "200" "Bedrock download URL should return HTTP 200" +} + +test_bedrock_primary_has_windows_entry() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local found + found=$(echo "$body" | jq -r '.result.links[] | select(.downloadType == "serverBedrockWin") | .downloadUrl // empty') + + if [[ -n "$found" && "$found" != "null" ]]; then + return 0 + else + echo "No serverBedrockWin entry found (may be expected on some API versions)" + return 77 # skip + fi +} + +test_bedrock_primary_download_type_values() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local types + types=$(echo "$body" | jq -r '.result.links[].downloadType' | sort -u) + + assert_not_empty "$types" "Should have at least one download type" +} + +# ---------- Version parsing from URL --------------------------------------- + +test_bedrock_version_from_url() { + local body + body=$(api_get_ua "$BEDROCK_API_URL") + local url + url=$(echo "$body" | jq -r '.result.links[] | select(.downloadType == "serverBedrockLinux") | .downloadUrl // empty') + + if [[ -z "$url" ]]; then + echo "No Linux download URL found" + return 1 + fi + + # Extract version from URL (same regex as bedrock.sh) + local ver + ver=$(echo "$url" | grep -oP 'bedrock-server-\K[\d.]+(?=\.zip)') + assert_not_empty "$ver" "Should extract version from URL" + assert_matches "$ver" "^[0-9]+\.[0-9]+\.[0-9]+" \ + "Version '${ver}' should look like x.y.z" +} + +# ---------- Fallback API (GitHub) ------------------------------------------ + +test_bedrock_fallback_api_reachable() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + assert_not_empty "$body" "Bedrock fallback API body should not be empty" +} + +test_bedrock_fallback_api_is_json() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + assert_json_valid "$body" "Fallback API response should be valid JSON" +} + +test_bedrock_fallback_has_release() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + assert_json_field "$body" '.release' "Should have .release field" +} + +test_bedrock_fallback_release_is_object() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + local type + type=$(echo "$body" | jq -r '.release | type') + assert_equals "$type" "object" ".release should be a JSON object" +} + +test_bedrock_fallback_release_has_entries() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + local count + count=$(echo "$body" | jq '.release | length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: .release should have entries, got ${count}" + return 1 + fi +} + +test_bedrock_fallback_entry_has_linux_url() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + local first_key + first_key=$(echo "$body" | jq -r '.release | keys | .[0]') + local url + url=$(echo "$body" | jq -r ".release[\"${first_key}\"].linux.url // empty") + + assert_not_empty "$url" "First release entry should have .linux.url" +} + +test_bedrock_fallback_linux_url_format() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + local first_key + first_key=$(echo "$body" | jq -r '.release | keys | .[0]') + local url + url=$(echo "$body" | jq -r ".release[\"${first_key}\"].linux.url // empty") + + assert_matches "$url" "bedrock-server-.*\.zip$" \ + "Fallback URL should end with bedrock-server-{version}.zip" +} + +test_bedrock_fallback_highest_version() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + # The script sorts by version and takes the last (highest) entry + local highest + highest=$(echo "$body" | jq -r ' + .release | to_entries | sort_by(.key | split(".") | map(tonumber)) | last | .key + ') + + assert_not_empty "$highest" "Should find a highest version" + assert_matches "$highest" "^[0-9]+\.[0-9]+\.[0-9]+" \ + "Highest version '${highest}' should look like x.y.z" +} + +# ---------- Preview entry -------------------------------------------------- + +test_bedrock_fallback_has_preview() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + assert_json_field "$body" '.preview' "Should have .preview field" +} + +test_bedrock_fallback_preview_is_object() { + local body + body=$(api_get "$BEDROCK_FALLBACK_URL") + local type + type=$(echo "$body" | jq -r '.preview | type') + assert_equals "$type" "object" ".preview should be a JSON object" +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "Primary API is reachable" test_bedrock_primary_api_reachable +run_test "Primary API response is valid JSON" test_bedrock_primary_api_is_json +run_test "Primary API has .result field" test_bedrock_primary_api_has_result +run_test "Primary API has .result.links" test_bedrock_primary_api_has_links +run_test ".result.links is an array" test_bedrock_primary_api_links_is_array +run_test "Has serverBedrockLinux entry" test_bedrock_primary_has_linux_entry +run_test "Linux download URL has correct format" test_bedrock_primary_linux_url_format +run_test "Linux download URL returns HTTP 200" test_bedrock_primary_linux_url_reachable +run_test "Has serverBedrockWin entry" test_bedrock_primary_has_windows_entry +run_test "Download types are present" test_bedrock_primary_download_type_values +run_test "Version can be parsed from download URL" test_bedrock_version_from_url +run_test "Fallback API (GitHub) is reachable" test_bedrock_fallback_api_reachable +run_test "Fallback API response is valid JSON" test_bedrock_fallback_api_is_json +run_test "Fallback has .release field" test_bedrock_fallback_has_release +run_test ".release is a JSON object" test_bedrock_fallback_release_is_object +run_test ".release has entries" test_bedrock_fallback_release_has_entries +run_test "First release entry has .linux.url" test_bedrock_fallback_entry_has_linux_url +run_test "Fallback URL has correct format" test_bedrock_fallback_linux_url_format +run_test "Fallback has a highest version" test_bedrock_fallback_highest_version +run_test "Fallback has .preview field" test_bedrock_fallback_has_preview +run_test ".preview is a JSON object" test_bedrock_fallback_preview_is_object + +print_summary diff --git a/tests/test_fabric.sh b/tests/test_fabric.sh new file mode 100755 index 0000000..43adfe0 --- /dev/null +++ b/tests/test_fabric.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +# Tests for Fabric meta API +# Covers: Fabric loader version resolution, installer version, download URL +# Usage: ./tests/test_fabric.sh [mc_version] + +source "$(dirname "$0")/test_utils.sh" + +MC_VERSION="${1:-$(get_latest_mc_version)}" + +if [[ -z "$MC_VERSION" ]]; then + echo "Could not determine MC version" + exit 1 +fi + +echo -e "\n${BOLD}Fabric API Tests (MC ${MC_VERSION})${NC}" +echo "" + +# ---------- Fabric loader meta API ----------------------------------------- + +test_fabric_meta_reachable() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + assert_not_empty "$body" "Fabric meta body should not be empty" + assert_json_valid "$body" "Fabric meta should be valid JSON" +} + +test_fabric_meta_is_array() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local type + type=$(echo "$body" | jq -r 'type') + assert_equals "$type" "array" "Response should be a JSON array" +} + +test_fabric_meta_has_entries() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local count + count=$(echo "$body" | jq 'length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: Loader array should have entries, got ${count}" + return 1 + fi +} + +test_fabric_loader_entry_has_version() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + assert_json_field "$body" '.[0].loader.version' "Loader entry should have .loader.version" +} + +test_fabric_loader_version_is_semver() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local version + version=$(echo "$body" | jq -r '.[0].loader.version') + + assert_matches "$version" "^[0-9]+\.[0-9]+\.[0-9]+" \ + "Loader version '${version}' should be semver-like" +} + +test_fabric_entry_has_required_fields() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + assert_json_field "$body" '.[0].loader.version' "Should have loader version" + assert_json_field "$body" '.[0].loader.maven' "Should have loader maven coordinate" +} + +test_fabric_loader_maven_coordinate() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local maven + maven=$(echo "$body" | jq -r '.[0].loader.maven') + assert_matches "$maven" "^net.fabricmc:fabric-loader:" \ + "Maven coordinate should be net.fabricmc:fabric-loader:{version}" +} + +test_fabric_loader_has_stable_flag() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local stable + stable=$(echo "$body" | jq -r '.[0].loader.stable') + assert_matches "$stable" "^(true|false)$" "Stable flag should be true or false" +} + +test_fabric_has_intermediary() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + assert_json_field "$body" '.[0].intermediary.version' "Should have intermediary version" +} + +# ---------- Fabric installer metadata (Maven XML) ------------------------- + +test_fabric_installer_metadata_reachable() { + local body + body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + assert_not_empty "$body" "Installer metadata should not be empty" +} + +test_fabric_installer_metadata_is_xml() { + local body + body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + assert_contains "$body" "" "Should be valid XML with root" +} + +test_fabric_installer_has_latest() { + local body + body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + local latest + latest=$(echo "$body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + assert_not_empty "$latest" "Installer should have a latest version" +} + +test_fabric_installer_latest_is_semver() { + local body + body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + local latest + latest=$(echo "$body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + + assert_matches "$latest" "^[0-9]+\.[0-9]+\.[0-9]+" \ + "Installer version '${latest}' should be semver-like" +} + +# ---------- Fabric server jar download URL --------------------------------- + +test_fabric_download_url_format() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local loader_version + loader_version=$(echo "$body" | jq -r '.[0].loader.version') + + local installer_body + installer_body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + local installer_version + installer_version=$(echo "$installer_body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + + local expected_url="https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${loader_version}/${installer_version}/server/jar" + assert_not_empty "$expected_url" "Constructed URL should not be empty" + assert_matches "$expected_url" "^https://meta.fabricmc.net/v2/versions/loader/" \ + "URL should start with Fabric meta base" +} + +test_fabric_download_url_reachable() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local loader_version + loader_version=$(echo "$body" | jq -r '.[0].loader.version') + + local installer_body + installer_body=$(api_get "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml") + local installer_version + installer_version=$(echo "$installer_body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + + local url="https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${loader_version}/${installer_version}/server/jar" + assert_http_ok "$url" "Fabric server jar URL should return HTTP 200" +} + +# ---------- Specific version spot checks ----------------------------------- + +test_fabric_known_1_20_4() { + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/1.20.4") + local count + count=$(echo "$body" | jq 'length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: Fabric should support MC 1.20.4" + return 1 + fi +} + +test_fabric_latest_loader_version_gte() { + # The latest loader version should be >= 0.14.0 (a known old version) + local body + body=$(api_get "https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}") + local version + version=$(echo "$body" | jq -r '.[0].loader.version') + + assert_version_gte "$version" "0.14.0" "Latest loader version should be >= 0.14.0" +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "Fabric meta API is reachable and valid JSON" test_fabric_meta_reachable +run_test "Response is a JSON array" test_fabric_meta_is_array +run_test "Loader array has entries" test_fabric_meta_has_entries +run_test "Loader entry has .loader.version" test_fabric_loader_entry_has_version +run_test "Loader version is semver format" test_fabric_loader_version_is_semver +run_test "Loader entry has required fields (version, maven)" test_fabric_entry_has_required_fields +run_test "Loader maven coordinate is correct" test_fabric_loader_maven_coordinate +run_test "Loader has stable flag" test_fabric_loader_has_stable_flag +run_test "Entry has intermediary version" test_fabric_has_intermediary +run_test "Installer maven-metadata.xml is reachable" test_fabric_installer_metadata_reachable +run_test "Metadata is valid XML" test_fabric_installer_metadata_is_xml +run_test "Metadata has latest version" test_fabric_installer_has_latest +run_test "Installer latest version is semver" test_fabric_installer_latest_is_semver +run_test "Download URL format is correct" test_fabric_download_url_format +run_test "Download URL returns HTTP 200" test_fabric_download_url_reachable +run_test "Fabric supports MC 1.20.4" test_fabric_known_1_20_4 +run_test "Latest loader version >= 0.14.0" test_fabric_latest_loader_version_gte + +print_summary diff --git a/tests/test_forge.sh b/tests/test_forge.sh new file mode 100755 index 0000000..3868efc --- /dev/null +++ b/tests/test_forge.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# Tests for Minecraft Forge API +# Covers: Forge promotions API, version resolution, download URL, old/new forge logic +# Usage: ./tests/test_forge.sh [mc_version] + +source "$(dirname "$0")/test_utils.sh" + +MC_VERSION="${1:-$(get_latest_mc_version)}" + +if [[ -z "$MC_VERSION" ]]; then + echo "Could not determine MC version" + exit 1 +fi + +echo -e "\n${BOLD}Forge API Tests (MC ${MC_VERSION})${NC}" +echo "" + +# ---------- Forge promotions API ------------------------------------------- + +test_forge_promotions_reachable() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + assert_not_empty "$body" "Promotions body should not be empty" + assert_json_valid "$body" "Promotions should be valid JSON" +} + +test_forge_promotions_has_promos() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + assert_json_field "$body" '.promos' "Should have .promos field" +} + +test_forge_promos_is_object() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local type + type=$(echo "$body" | jq -r '.promos | type') + assert_equals "$type" "object" ".promos should be a JSON object" +} + +test_forge_promos_has_known_key() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local known_key="1.20.4-latest" + local found + found=$(echo "$body" | jq -r ".promos[\"${known_key}\"] // empty") + + if [[ -n "$found" && "$found" != "null" ]]; then + return 0 + else + echo "ASSERT FAILED: promos should have '1.20.4-latest' key" + return 1 + fi +} + +test_forge_promos_value_format() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local known_key="1.20.4-latest" + local version + version=$(echo "$body" | jq -r ".promos[\"${known_key}\"] // empty") + + # Forge versions look like "20.4.234" or "14.23.5.2860" + assert_matches "$version" "^[0-9]+\.[0-9]+\.[0-9]+" \ + "Forge version '${version}' should look like x.y.z" +} + +# ---------- Forge download URL construction -------------------------------- + +test_forge_download_url_format() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r '.promos["1.20.4-latest"] // empty') + + if [[ -z "$version" ]]; then + echo "Could not determine Forge version for 1.20.4" + return 1 + fi + + local expected_url="https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.4-${version}/forge-1.20.4-${version}-installer.jar" + assert_not_empty "$expected_url" "Download URL should not be empty" + assert_matches "$expected_url" "maven.minecraftforge.net.*installer\.jar" \ + "URL should point to Forge maven installer" +} + +test_forge_download_url_reachable() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r '.promos["1.20.4-latest"] // empty') + + if [[ -z "$version" ]]; then + echo "Could not determine Forge version for 1.20.4" + return 1 + fi + + local url="https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.4-${version}/forge-1.20.4-${version}-installer.jar" + assert_http_ok "$url" "Forge installer URL should return HTTP 200" +} + +# ---------- Old vs New Forge version logic --------------------------------- +# Forge >= 1.17.1 uses the new launcher (unix_args.txt) +# Forge < 1.17.1 uses the old launcher (server.jar symlink) + +test_forge_new_version_boundary() { + # This replicates the logic from common.sh newForge() + local test_version="1.17.1" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + assert_equals "$result" "1.17.1" "1.17.1 >= 1.17.1 (boundary case)" +} + +test_forge_old_version_is_old() { + local test_version="1.16.5" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + # When sort -V puts test_version first, it's < 1.17.1 + assert_equals "$result" "$test_version" "1.16.5 < 1.17.1 (old forge)" +} + +test_forge_new_version_is_new() { + local test_version="1.20.4" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + # When sort -V puts 1.17.1 first, test_version is >= 1.17.1 + assert_equals "$result" "1.17.1" "1.20.4 >= 1.17.1 (new forge)" +} + +test_forge_1_17_exactly_is_old() { + local test_version="1.17" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + # 1.17 < 1.17.1 + assert_equals "$result" "$test_version" "1.17 < 1.17.1 (old forge, edge case)" +} + +test_forge_1_18_is_new() { + local test_version="1.18" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + assert_equals "$result" "1.17.1" "1.18 >= 1.17.1 (new forge)" +} + +test_forge_1_21_is_new() { + local test_version="1.21.1" + local result + result=$(printf '%s\n' "1.17.1" "$test_version" | sort -V | head -n1) + assert_equals "$result" "1.17.1" "1.21.1 >= 1.17.1 (new forge)" +} + +# ---------- Forge Maven metadata ------------------------------------------- + +test_forge_maven_metadata_reachable() { + local body + body=$(api_get "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml") + assert_not_empty "$body" "Forge maven metadata should be reachable" +} + +test_forge_maven_metadata_is_xml() { + local body + body=$(api_get "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml") + assert_contains "$body" "" "Should be valid XML" +} + +test_forge_maven_has_versions() { + local body + body=$(api_get "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml") + assert_contains "$body" "" "Should have element" +} + +# ---------- Forge version resolution for specific MC versions -------------- + +test_forge_supports_1_20_4() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r '.promos["1.20.4-latest"] // empty') + assert_not_empty "$version" "Forge should support MC 1.20.4" +} + +test_forge_supports_1_19_2() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r '.promos["1.19.2-latest"] // empty') + assert_not_empty "$version" "Forge should support MC 1.19.2" +} + +test_forge_supports_1_12_2() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r '.promos["1.12.2-latest"] // empty') + assert_not_empty "$version" "Forge should support MC 1.12.2 (classic version)" +} + +test_forge_latest_matches_current_mc() { + local body + body=$(api_get "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json") + local version + version=$(echo "$body" | jq -r ".promos[\"${MC_VERSION}-latest\"] // empty") + # This may not exist for all MC versions, so we treat it as informational + if [[ -n "$version" && "$version" != "null" ]]; then + echo "Forge ${MC_VERSION}-latest = ${version}" + return 0 + else + echo "No forge ${MC_VERSION}-latest in promos (may be expected)" + return 77 # skip + fi +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "Forge promotions API is reachable" test_forge_promotions_reachable +run_test "Promotions JSON has .promos field" test_forge_promotions_has_promos +run_test ".promos is a JSON object" test_forge_promos_is_object +run_test "Promos has '1.20.4-latest' key" test_forge_promos_has_known_key +run_test "Promos value has Forge version format" test_forge_promos_value_format +run_test "Download URL matches Forge maven pattern" test_forge_download_url_format +run_test "Download URL returns HTTP 200" test_forge_download_url_reachable +run_test "1.17.1 boundary case (new forge)" test_forge_new_version_boundary +run_test "1.16.5 is old forge" test_forge_old_version_is_old +run_test "1.20.4 is new forge" test_forge_new_version_is_new +run_test "1.17 is old forge (edge case)" test_forge_1_17_exactly_is_old +run_test "1.18 is new forge" test_forge_1_18_is_new +run_test "1.21.1 is new forge" test_forge_1_21_is_new +run_test "Forge maven metadata is reachable" test_forge_maven_metadata_reachable +run_test "Maven metadata is valid XML" test_forge_maven_metadata_is_xml +run_test "Maven metadata has versions" test_forge_maven_has_versions +run_test "Forge supports MC 1.20.4" test_forge_supports_1_20_4 +run_test "Forge supports MC 1.19.2" test_forge_supports_1_19_2 +run_test "Forge supports MC 1.12.2 (classic)" test_forge_supports_1_12_2 +run_test "Forge latest matches current MC version" test_forge_latest_matches_current_mc + +print_summary diff --git a/tests/test_mojang.sh b/tests/test_mojang.sh new file mode 100755 index 0000000..9d61b40 --- /dev/null +++ b/tests/test_mojang.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# Tests for Mojang version manifest API +# Covers: vanilla server downloads, Java version detection +# Usage: ./tests/test_mojang.sh [mc_version] + +source "$(dirname "$0")/test_utils.sh" + +MC_VERSION="${1:-$(get_latest_mc_version)}" + +if [[ -z "$MC_VERSION" ]]; then + echo "Could not determine MC version" + exit 1 +fi + +echo -e "\n${BOLD}Mojang API Tests (MC ${MC_VERSION})${NC}" +echo "" + +# ---------- Mojang manifest ------------------------------------------------ + +test_manifest_reachable() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + assert_not_empty "$body" "Manifest body should not be empty" + assert_json_valid "$body" "Manifest should be valid JSON" +} + +test_manifest_has_latest() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + assert_json_field "$body" '.latest.release' "Manifest should have .latest.release" + assert_json_field "$body" '.latest.snapshot' "Manifest should have .latest.snapshot" +} + +test_manifest_has_versions_array() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + local count + count=$(echo "$body" | jq '.versions | length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: versions array should have entries, got ${count}" + return 1 + fi +} + +test_manifest_version_entry_format() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + # Each version entry should have id, type, url, time, releaseTime + local first_entry + first_entry=$(echo "$body" | jq '.versions[0]') + assert_json_field "$body" '.versions[0].id' "Version entry should have .id" + assert_json_field "$body" '.versions[0].type' "Version entry should have .type" + assert_json_field "$body" '.versions[0].url' "Version entry should have .url" +} + +test_manifest_contains_mc_version() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + local found + found=$(echo "$body" | jq -r ".versions[] | select(.id == \"${MC_VERSION}\") | .id") + assert_equals "$found" "$MC_VERSION" "Manifest should contain MC ${MC_VERSION}" +} + +# ---------- Version data (per-version manifest) ---------------------------- + +get_version_url() { + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + echo "$body" | jq -r ".versions[] | select(.id == \"${MC_VERSION}\") | .url" +} + +test_version_data_reachable() { + local version_url + version_url=$(get_version_url) + assert_not_empty "$version_url" "Version URL should exist for ${MC_VERSION}" + + local body + body=$(api_get "$version_url") + assert_not_empty "$body" "Version data body should not be empty" + assert_json_valid "$body" "Version data should be valid JSON" +} + +test_version_data_has_downloads() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + assert_json_field "$body" '.downloads.server.url' "Version data should have .downloads.server.url" +} + +test_version_data_has_java_version() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + assert_json_field "$body" '.javaVersion.majorVersion' "Version data should have .javaVersion.majorVersion" +} + +test_version_data_has_client() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + assert_json_field "$body" '.downloads.client.url' "Version data should have .downloads.client.url" +} + +# ---------- Server jar URL validation -------------------------------------- + +test_server_jar_url_format() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + local server_url + server_url=$(echo "$body" | jq -r '.downloads.server.url') + + assert_matches "$server_url" "^https://(.*minecraft\\.net|piston-data\\.mojang\\.com)/" \ + "Server URL should be a Mojang CDN or piston-data URL" +} + +test_server_jar_url_reachable() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + local server_url + server_url=$(echo "$body" | jq -r '.downloads.server.url') + + assert_http_ok "$server_url" "Server jar URL should return HTTP 200" +} + +# ---------- Java version detection ----------------------------------------- + +test_java_version_is_numeric() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + local java_major + java_major=$(echo "$body" | jq -r '.javaVersion.majorVersion') + + assert_matches "$java_major" "^[0-9]+$" "Java major version should be numeric" +} + +test_java_version_known_values() { + local version_url + version_url=$(get_version_url) + local body + body=$(api_get "$version_url") + local java_major + java_major=$(echo "$body" | jq -r '.javaVersion.majorVersion') + + case "$java_major" in + 8|16|17|21) return 0 ;; + *) + echo "ASSERT FAILED: Java version ${java_major} is not a known expected value (8,16,17,21)" + echo " This may be valid for a new MC release, but verify it." + return 1 + ;; + esac +} + +# ---------- Specific version spot checks ----------------------------------- + +test_known_old_version_java_8() { + # MC 1.16.5 should use Java 8 + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + local version_url + version_url=$(echo "$body" | jq -r '.versions[] | select(.id == "1.16.5") | .url') + + if [[ -z "$version_url" || "$version_url" == "null" ]]; then + echo "Could not fetch 1.16.5 version data" + return 1 + fi + + local vdata + vdata=$(api_get "$version_url") + local java_major + java_major=$(echo "$vdata" | jq -r '.javaVersion.majorVersion') + assert_equals "$java_major" "8" "MC 1.16.5 should use Java 8" +} + +test_known_new_version_java_21() { + # MC 1.20.5+ should use Java 21 + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + local version_url + version_url=$(echo "$body" | jq -r '.versions[] | select(.id == "1.21.1") | .url') + + if [[ -z "$version_url" || "$version_url" == "null" ]]; then + echo "Could not fetch 1.21.1 version data" + return 1 + fi + + local vdata + vdata=$(api_get "$version_url") + local java_major + java_major=$(echo "$vdata" | jq -r '.javaVersion.majorVersion') + assert_equals "$java_major" "21" "MC 1.21.1 should use Java 21" +} + +test_known_1_18_version_java_17() { + # MC 1.18.x should use Java 17 + local body + body=$(api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json") + local version_url + version_url=$(echo "$body" | jq -r '.versions[] | select(.id == "1.18.2") | .url') + + if [[ -z "$version_url" || "$version_url" == "null" ]]; then + echo "Could not fetch 1.18.2 version data" + return 1 + fi + + local vdata + vdata=$(api_get "$version_url") + local java_major + java_major=$(echo "$vdata" | jq -r '.javaVersion.majorVersion') + assert_equals "$java_major" "17" "MC 1.18.2 should use Java 17" +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "Mojang manifest is reachable and valid JSON" test_manifest_reachable +run_test "Manifest has .latest.release and .latest.snapshot" test_manifest_has_latest +run_test "Manifest .versions array is non-empty" test_manifest_has_versions_array +run_test "Manifest version entries have required fields" test_manifest_version_entry_format +run_test "Manifest contains MC ${MC_VERSION}" test_manifest_contains_mc_version +run_test "Version data for ${MC_VERSION} is reachable" test_version_data_reachable +run_test "Version data has .downloads.server.url" test_version_data_has_downloads +run_test "Version data has .javaVersion.majorVersion" test_version_data_has_java_version +run_test "Version data has .downloads.client.url" test_version_data_has_client +run_test "Server jar URL matches Mojang CDN pattern" test_server_jar_url_format +run_test "Server jar URL returns HTTP 200" test_server_jar_url_reachable +run_test "Java version is numeric" test_java_version_is_numeric +run_test "Java version is a known value (8/16/17/21)" test_java_version_known_values +run_test "MC 1.16.5 uses Java 8" test_known_old_version_java_8 +run_test "MC 1.21.1 uses Java 21" test_known_new_version_java_21 +run_test "MC 1.18.2 uses Java 17" test_known_1_18_version_java_17 + +print_summary diff --git a/tests/test_neoforge.sh b/tests/test_neoforge.sh new file mode 100755 index 0000000..fcb17ef --- /dev/null +++ b/tests/test_neoforge.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +# Tests for NeoForge API +# Covers: NeoForge maven metadata, version resolution, download URL +# Usage: ./tests/test_neoforge.sh [mc_version] + +source "$(dirname "$0")/test_utils.sh" + +MC_VERSION="${1:-$(get_latest_mc_version)}" + +if [[ -z "$MC_VERSION" ]]; then + echo "Could not determine MC version" + exit 1 +fi + +echo -e "\n${BOLD}NeoForge API Tests (MC ${MC_VERSION})${NC}" +echo "" + +# ---------- NeoForge Maven metadata ---------------------------------------- + +test_neoforge_metadata_reachable() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + assert_not_empty "$body" "NeoForge maven metadata should be reachable" +} + +test_neoforge_metadata_is_xml() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + assert_contains "$body" "" "Should be valid XML" +} + +test_neoforge_metadata_has_versions() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + assert_contains "$body" "" "Should have element" +} + +test_neoforge_metadata_has_latest() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local latest + latest=$(echo "$body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + assert_not_empty "$latest" "Metadata should have a latest version" +} + +test_neoforge_metadata_has_release() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local release + release=$(echo "$body" | xmllint --xpath "/metadata/versioning/release/text()" - 2>/dev/null) + assert_not_empty "$release" "Metadata should have a release version" +} + +# ---------- NeoForge version resolution ------------------------------------ +# NeoForge versions start with MC minor version (e.g., MC 1.20.4 -> 20.4.x) + +test_neoforge_version_prefix() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + # Extract MC minor from version (e.g., "1.21.1" -> "21.1") + local mc_minor="${MC_VERSION#1.}" + + # Check that there are versions matching this MC minor prefix + local matching + matching=$(echo "$versions" | tr ' ' '\n' | grep "^${mc_minor}\." | head -1) + + assert_not_empty "$matching" \ + "Should find at least one NeoForge version starting with ${mc_minor}" +} + +test_neoforge_stable_versions_exist() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local mc_minor="${MC_VERSION#1.}" + local stable_count + stable_count=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep -c "^${mc_minor}\." || true) + + if [[ "$stable_count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: No stable NeoForge versions found for MC minor ${mc_minor}" + echo " This may be valid for a very new MC version" + return 1 + fi +} + +test_neoforge_no_beta_in_stable() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local mc_minor="${MC_VERSION#1.}" + local stable_versions + stable_versions=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep "^${mc_minor}\." | sort -V | tail -5) + + # None of the stable versions should contain beta or alpha + local bad_count + bad_count=$(echo "$stable_versions" | grep -c -- '-beta\|-alpha' || true) + assert_equals "$bad_count" "0" "Stable versions should not contain beta/alpha" +} + +test_neoforge_latest_for_mc_is_stable() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local mc_minor="${MC_VERSION#1.}" + local latest_for_mc + latest_for_mc=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep "^${mc_minor}\." | sort -V | tail -1) + + if [[ -z "$latest_for_mc" ]]; then + echo "No stable NeoForge version found for MC ${MC_VERSION}" + return 1 + fi + + local bad=$(echo "$latest_for_mc" | grep -c -- '-beta\|-alpha' || true) + assert_equals "$bad" "0" "Latest stable version for MC ${MC_VERSION} (${latest_for_mc}) should not contain beta/alpha" +} + +# ---------- NeoForge download URL ------------------------------------------ + +_neoforge_latest_for_mc() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + local mc_minor="${MC_VERSION#1.}" + echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep "^${mc_minor}\." | sort -V | tail -1 +} + +test_neoforge_download_url_format() { + local latest + latest=$(_neoforge_latest_for_mc) + + if [[ -z "$latest" ]]; then + echo "No stable NeoForge version found for MC ${MC_VERSION}" + return 1 + fi + + local expected_url="https://maven.neoforged.net/releases/net/neoforged/neoforge/${latest}/neoforge-${latest}-installer.jar" + assert_matches "$expected_url" "maven.neoforged.net.*installer\.jar" \ + "URL should point to NeoForge maven installer" +} + +test_neoforge_download_url_reachable() { + local latest + latest=$(_neoforge_latest_for_mc) + + if [[ -z "$latest" ]]; then + echo "No stable NeoForge version found for MC ${MC_VERSION}" + return 1 + fi + + local url="https://maven.neoforged.net/releases/net/neoforged/neoforge/${latest}/neoforge-${latest}-installer.jar" + assert_http_ok "$url" "NeoForge installer URL should return HTTP 200" +} + +# ---------- NeoForge version sort check ------------------------------------ + +test_neoforge_versions_sorted_ascending() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local mc_minor="${MC_VERSION#1.}" + local last_version + last_version=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep "^${mc_minor}\." | sort -V | tail -1) + + # The last version after sort -V should be the same as the latest from metadata + local meta_latest + meta_latest=$(echo "$body" | xmllint --xpath "/metadata/versioning/latest/text()" - 2>/dev/null) + + if [[ "$last_version" == "$meta_latest" ]]; then + return 0 + else + echo "ASSERT FAILED: sort -V last version (${last_version}) != metadata latest (${meta_latest})" + echo " This is informational - metadata latest may include beta" + return 77 # skip + fi +} + +# ---------- Specific MC version support ------------------------------------ + +test_neoforge_supports_1_21_1() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local count + count=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep -c "^21.1\." || true) + + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: No NeoForge versions found for MC 1.21.1 (21.1.x)" + return 1 + fi +} + +test_neoforge_supports_1_20_4() { + local body + body=$(api_get "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml") + local versions + versions=$(echo "$body" | xmllint --xpath "/metadata/versioning/versions/version/text()" - 2>/dev/null) + + local count + count=$(echo "$versions" | tr ' ' '\n' | grep -v -- '-beta\|-alpha' | grep -c "^20.4\." || true) + + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: No NeoForge versions found for MC 1.20.4 (20.4.x)" + return 1 + fi +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "NeoForge maven metadata is reachable" test_neoforge_metadata_reachable +run_test "Metadata is valid XML" test_neoforge_metadata_is_xml +run_test "Metadata has element" test_neoforge_metadata_has_versions +run_test "Metadata has version" test_neoforge_metadata_has_latest +run_test "Metadata has version" test_neoforge_metadata_has_release +run_test "Versions exist with MC minor prefix" test_neoforge_version_prefix +run_test "Stable versions exist for current MC" test_neoforge_stable_versions_exist +run_test "Stable versions have no beta/alpha tags" test_neoforge_no_beta_in_stable +run_test "Latest stable version for MC is stable" test_neoforge_latest_for_mc_is_stable +run_test "Download URL matches NeoForge maven pattern" test_neoforge_download_url_format +run_test "Download URL returns HTTP 200" test_neoforge_download_url_reachable +run_test "Versions are sorted in ascending order" test_neoforge_versions_sorted_ascending +run_test "NeoForge supports MC 1.21.1 (21.1.x)" test_neoforge_supports_1_21_1 +run_test "NeoForge supports MC 1.20.4 (20.4.x)" test_neoforge_supports_1_20_4 + +print_summary diff --git a/tests/test_paper.sh b/tests/test_paper.sh new file mode 100755 index 0000000..0b63632 --- /dev/null +++ b/tests/test_paper.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# Tests for PaperMC Downloads Service API (v3) +# Covers: API reachability, build resolution, download URL, checksums +# Usage: ./tests/test_paper.sh [mc_version] + +source "$(dirname "$0")/test_utils.sh" + +MC_VERSION="${1:-$(get_latest_mc_version)}" + +if [[ -z "$MC_VERSION" ]]; then + echo "Could not determine MC version" + exit 1 +fi + +echo -e "\n${BOLD}PaperMC API Tests (MC ${MC_VERSION})${NC}" +echo "" + +PAPER_API="https://fill.papermc.io/v3" +PAPER_UA="dockercraft-test/1.0" + +paper_api_get() { + curl -sf --connect-timeout 10 --max-time 30 \ + -H "User-Agent: ${PAPER_UA}" "$1" 2>/dev/null || true +} + +# ---------- Project info endpoint ------------------------------------------ + +test_project_api_reachable() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper") + assert_not_empty "$body" "Project API body should not be empty" + assert_json_valid "$body" "Project API should return valid JSON" +} + +test_project_has_versions() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper") + assert_json_field "$body" '.versions' "Should have .versions field" +} + +test_project_versions_is_object() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper") + local type + type=$(echo "$body" | jq -r '.versions | type') + assert_equals "$type" "object" ".versions should be a JSON object" +} + +test_project_has_version_groups() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper") + local count + count=$(echo "$body" | jq '.versions | length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: .versions should have groups, got ${count}" + return 1 + fi +} + +# ---------- Builds endpoint ------------------------------------------------- + +test_builds_api_reachable() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + assert_not_empty "$body" "Builds API body should not be empty" +} + +test_builds_is_array() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local type + type=$(echo "$body" | jq -r 'type') + assert_equals "$type" "array" "Response should be a JSON array" +} + +test_builds_has_entries() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local count + count=$(echo "$body" | jq 'length') + if [[ "$count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: builds array should have entries, got ${count}" + return 1 + fi +} + +test_build_entry_has_id() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + assert_json_field "$body" '.[0].id' "Build entry should have .id field" +} + +test_build_id_is_numeric() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local id + id=$(echo "$body" | jq -r '.[0].id') + assert_matches "$id" "^[0-9]+$" "Build id '${id}' should be numeric" +} + +test_build_entry_has_channel() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + assert_json_field "$body" '.[0].channel' "Build entry should have .channel field" +} + +test_build_channel_is_valid() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local channel + channel=$(echo "$body" | jq -r '.[0].channel') + assert_matches "$channel" "^(STABLE|EXPERIMENTAL)$" \ + "Build channel '${channel}' should be STABLE or EXPERIMENTAL" +} + +test_build_entry_has_downloads() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + assert_json_field "$body" '.[0].downloads."server:default"' \ + "Build entry should have .downloads.\"server:default\"" +} + +# ---------- Stable builds -------------------------------------------------- + +test_stable_builds_exist() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local stable_count + stable_count=$(echo "$body" | jq '[.[] | select(.channel == "STABLE")] | length') + if [[ "$stable_count" -gt 0 ]]; then + return 0 + else + echo "ASSERT FAILED: No stable builds found for MC ${MC_VERSION}" + return 1 + fi +} + +test_latest_stable_build_id() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local id + id=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE")) | .id') + + assert_not_empty "$id" "Latest stable build id should not be empty" + assert_matches "$id" "^[0-9]+$" "Build id should be numeric" +} + +# ---------- Download URL --------------------------------------------------- + +test_download_url_present() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local url + url=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty') + + assert_not_empty "$url" "Stable build download URL should not be empty" +} + +test_download_url_format() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local url + url=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty') + + assert_matches "$url" ".*paper-.*\.jar$" \ + "Download URL should end with a paper jar filename" +} + +test_download_url_reachable() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local url + url=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty') + + if [[ -z "$url" ]]; then + echo "No download URL found" + return 1 + fi + + assert_http_ok "$url" "Download URL should return HTTP 200" +} + +test_download_name_matches_pattern() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local name + name=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".name) // empty') + + assert_matches "$name" "^paper-${MC_VERSION}-[0-9]+\.jar$" \ + "Download name '${name}' should match paper-{version}-{build}.jar" +} + +# ---------- Checksums ------------------------------------------------------ + +test_download_has_sha256() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local sha + sha=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".checksums.sha256) // empty') + + assert_matches "$sha" "^[a-f0-9]{64}$" \ + "SHA256 '${sha}' should be a 64-char hex string" +} + +test_download_has_size() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local size + size=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".size) // empty') + + assert_matches "$size" "^[0-9]+$" "Download size '${size}' should be numeric" +} + +test_download_size_reasonable() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local size + size=$(echo "$body" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".size)') + + # Paper jar should be between 1MB and 100MB + if [[ "$size" -gt 1000000 && "$size" -lt 100000000 ]]; then + return 0 + else + echo "ASSERT FAILED: Download size ${size} bytes is outside expected range (1MB-100MB)" + return 1 + fi +} + +# ---------- Build ordering ------------------------------------------------- + +test_builds_are_ordered() { + local body + body=$(paper_api_get "${PAPER_API}/projects/paper/versions/${MC_VERSION}/builds") + local first_id last_id + first_id=$(echo "$body" | jq '.[0].id') + last_id=$(echo "$body" | jq '.[-1].id') + + # v3 API returns builds in descending order (newest first) + assert_version_gte "$first_id" "$last_id" "Build ids should be in descending order" +} + +# ---------- Run all tests -------------------------------------------------- + +run_test "Project API is reachable and valid JSON" test_project_api_reachable +run_test "Project has .versions field" test_project_has_versions +run_test ".versions is a JSON object" test_project_versions_is_object +run_test "Project has version groups" test_project_has_version_groups +run_test "Builds API is reachable" test_builds_api_reachable +run_test "Response is a JSON array" test_builds_is_array +run_test "Builds array has entries" test_builds_has_entries +run_test "Build entry has .id field" test_build_entry_has_id +run_test "Build id is numeric" test_build_id_is_numeric +run_test "Build entry has .channel field" test_build_entry_has_channel +run_test "Build channel is STABLE or EXPERIMENTAL" test_build_channel_is_valid +run_test "Build entry has downloads" test_build_entry_has_downloads +run_test "Stable builds exist for MC ${MC_VERSION}" test_stable_builds_exist +run_test "Latest stable build id is numeric" test_latest_stable_build_id +run_test "Download URL is present" test_download_url_present +run_test "Download URL has correct format" test_download_url_format +run_test "Download URL returns HTTP 200" test_download_url_reachable +run_test "Download name matches paper-{ver}-{build}.jar" test_download_name_matches_pattern +run_test "Download has SHA256 checksum" test_download_has_sha256 +run_test "Download has file size" test_download_has_size +run_test "Download size is reasonable (1MB-100MB)" test_download_size_reasonable +run_test "Build ids are in descending order (newest first)" test_builds_are_ordered + +print_summary diff --git a/tests/test_utils.sh b/tests/test_utils.sh new file mode 100755 index 0000000..e2d7b23 --- /dev/null +++ b/tests/test_utils.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +# Shared test utilities for Dockercraft API tests +# Source this file in individual test scripts: source "$(dirname "$0")/test_utils.sh" + +set -euo pipefail + +# ---------- colors --------------------------------------------------------- + +if [[ -t 1 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + BOLD='\033[1m' + NC='\033[0m' +else + RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' +fi + +# ---------- counters ------------------------------------------------------- + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_SKIPPED=0 +FAILED_TESTS=() + +# ---------- test framework ------------------------------------------------- + +# Run a test function and track results +# Usage: run_test "test name" test_function_name +# Exit codes: 0=pass, 1=fail, 77=skip +# IMPORTANT: Test functions MUST use `exit 77` (not `return 77`) to signal skip. +# `return 77` does not propagate through command substitution with `set -e`. +run_test() { + local name="$1" + local func="$2" + local extra_args="${3:-}" + + TESTS_RUN=$((TESTS_RUN + 1)) + + # Run the test function, capturing output + local output + local exit_code=0 + if [[ -n "$extra_args" ]]; then + output=$("$func" "$extra_args" 2>&1) || exit_code=$? + else + output=$("$func" 2>&1) || exit_code=$? + fi + + if [[ $exit_code -eq 0 ]]; then + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo -e " ${GREEN}✓${NC} ${name}" + if [[ -n "$output" ]]; then + echo -e " ${output}" + fi + elif [[ $exit_code -eq 77 ]]; then + TESTS_SKIPPED=$((TESTS_SKIPPED + 1)) + echo -e " ${YELLOW}⊘${NC} ${name} ${YELLOW}(skipped)${NC}" + if [[ -n "$output" ]]; then + echo -e " ${output}" + fi + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_TESTS+=("$name") + echo -e " ${RED}✗${NC} ${name}" + if [[ -n "$output" ]]; then + echo -e " ${output}" + fi + fi +} + +# Assert that a string contains a substring +# Usage: assert_contains "$haystack" "$needle" ["message"] +assert_contains() { + local haystack="$1" + local needle="$2" + local message="${3:-Expected to contain '${needle}'}" + + if [[ "$haystack" == *"$needle"* ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " Got: ${haystack:0:200}" + return 1 + fi +} + +# Assert that a string matches a regex +# Usage: assert_matches "$string" "$regex" ["message"] +assert_matches() { + local string="$1" + local regex="$2" + local message="${3:-Expected to match regex '${regex}'}" + + if [[ "$string" =~ $regex ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " Got: ${string:0:200}" + return 1 + fi +} + +# Assert that two strings are equal +# Usage: assert_equals "$actual" "$expected" ["message"] +assert_equals() { + local actual="$1" + local expected="$2" + local message="${3:-Expected '${expected}', got '${actual}'}" + + if [[ "$actual" == "$expected" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " Expected: ${expected:0:200}" + echo " Got: ${actual:0:200}" + return 1 + fi +} + +# Assert that a string is not empty +# Usage: assert_not_empty "$value" ["message"] +assert_not_empty() { + local value="$1" + local message="${2:-Expected non-empty value}" + + if [[ -n "$value" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + return 1 + fi +} + +# Assert that a string is empty +# Usage: assert_empty "$value" ["message"] +assert_empty() { + local value="$1" + local message="${2:-Expected empty value}" + + if [[ -z "$value" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " Got: ${value:0:200}" + return 1 + fi +} + +# Assert that a JSON field exists and is non-null +# Usage: assert_json_field "$json" "$jq_expr" ["message"] +assert_json_field() { + local json="$1" + local jq_expr="$2" + local message="${3:-Expected JSON field '${jq_expr}' to exist}" + + local value + value=$(echo "$json" | jq -r "$jq_expr" 2>/dev/null) || true + + if [[ -n "$value" && "$value" != "null" && "$value" != "" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " jq expression: ${jq_expr}" + echo " Result: ${value:-}" + return 1 + fi +} + +# Assert that a URL returns HTTP 200 +# Usage: assert_http_ok "$url" ["message"] +assert_http_ok() { + local url="$1" + local message="${2:-Expected HTTP 200 from ${url}}" + + local http_code + http_code=$(curl -sL -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "$url") || true + + if [[ "$http_code" == "200" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " URL: ${url}" + echo " HTTP status: ${http_code}" + return 1 + fi +} + +# Assert that JSON parses successfully and has a given top-level key +# Usage: assert_json_valid "$json" ["message"] +assert_json_valid() { + local json="$1" + local message="${2:-Expected valid JSON}" + + if echo "$json" | jq empty 2>/dev/null; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " First 200 chars: ${json:0:200}" + return 1 + fi +} + +# Version comparison: assert version A >= version B +# Usage: assert_version_gte "$version_a" "$version_b" +assert_version_gte() { + local v_a="$1" + local v_b="$2" + local message="${3:-Expected ${v_a} >= ${v_b}}" + + local result + result=$(printf '%s\n' "$v_b" "$v_a" | sort -V | head -n1) + + if [[ "$result" == "$v_b" ]]; then + return 0 + else + echo "ASSERT FAILED: ${message}" + echo " ${v_a} is not >= ${v_b}" + return 1 + fi +} + +# Version comparison: assert version A <= version B +assert_version_lte() { + assert_version_gte "$2" "$1" "${3:-Expected ${1} <= ${2}}" +} + +# ---------- API helpers ---------------------------------------------------- + +# Fetch a URL and return the body, or empty string on failure +# Usage: api_get "$url" +api_get() { + curl -sf --connect-timeout 10 --max-time 30 "$1" 2>/dev/null || true +} + +# Fetch a URL with custom headers and return the body +# Usage: api_get_ua "$url" ["user_agent"] +api_get_ua() { + local ua="${2:-Mozilla/5.0 (X11; Linux x86_64)}" + curl -sL -A "$ua" --connect-timeout 10 --max-time 30 "$1" 2>/dev/null || true +} + +# Get the latest MC version from Mojang manifest +# Usage: get_latest_mc_version +get_latest_mc_version() { + api_get "https://launchermeta.mojang.com/mc/game/version_manifest.json" | + jq -r '.latest.release' +} + +# ---------- summary -------------------------------------------------------- + +print_summary() { + echo "" + echo -e "${BOLD}========================================${NC}" + echo -e "${BOLD} Test Results${NC}" + echo -e "${BOLD}========================================${NC}" + echo -e " Total: ${TESTS_RUN}" + echo -e " ${GREEN}Passed: ${TESTS_PASSED}${NC}" + if [[ $TESTS_FAILED -gt 0 ]]; then + echo -e " ${RED}Failed: ${TESTS_FAILED}${NC}" + else + echo -e " Failed: 0" + fi + if [[ $TESTS_SKIPPED -gt 0 ]]; then + echo -e " ${YELLOW}Skipped: ${TESTS_SKIPPED}${NC}" + fi + echo -e "${BOLD}========================================${NC}" + + if [[ $TESTS_FAILED -gt 0 ]]; then + echo "" + echo -e "${RED}Failed tests:${NC}" + for t in "${FAILED_TESTS[@]}"; do + echo -e " ${RED}✗ ${t}${NC}" + done + return 1 + fi + + echo -e "\n${GREEN}All tests passed!${NC}" + return 0 +}