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
This commit is contained in:
2026-08-21 12:49:27 -04:00
parent a6f610de28
commit 5688b30892
26 changed files with 2530 additions and 263 deletions
+22
View File
@@ -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
+2
View File
@@ -23,3 +23,5 @@ jobs:
context: .
push: true
tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:dev
cache-from: type=gha
cache-to: type=gha,mode=max
+3 -1
View File
@@ -24,4 +24,6 @@ jobs:
with:
context: .
push: true
tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:latest
tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
+17 -1
View File
@@ -1 +1,17 @@
data/
# Runtime data
data/
# Editor/IDE
*.swp
*.swo
*~
.vscode/
.idea/
*.iml
# OS
.DS_Store
Thumbs.db
# Test artifacts
*.log
+9 -2
View File
@@ -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"]
+163 -10
View File
@@ -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.
+15 -2
View File
@@ -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:
+49 -17
View File
@@ -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
+38 -24
View File
@@ -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 <<EOF > 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 <<EOF > server.info
MC_VERSION=$MC_VERSION
cat <<EOF > /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
}
+19 -8
View File
@@ -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"
}
+14 -6
View File
@@ -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() {
+25 -14
View File
@@ -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."
}
+35 -32
View File
@@ -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
}
+43 -28
View File
@@ -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."
}
+51 -15
View File
@@ -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."
}
+34 -9
View File
@@ -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."
}
+62 -69
View File
@@ -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
}
+23 -25
View File
@@ -11,6 +11,7 @@ Options:
-m, --mc-version <ver> Minecraft version (default: auto-detect latest)
-l, --loader <name> Loader to test (default: all)
-v, --loader-version <v> Loader version (default: auto-detect latest)
-t, --timeout <sec> 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
+174
View File
@@ -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 <<EOF
Usage: $(basename "$0") [options]
Run API health and correctness tests for Dockercraft.
Options:
-m, --mc-version <ver> Minecraft version (default: auto-detect latest)
-s, --suite <name> 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
+252
View File
@@ -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
+205
View File
@@ -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" "<metadata>" "Should be valid XML with <metadata> 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
+236
View File
@@ -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" "<metadata>" "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" "<versions>" "Should have <versions> 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
+243
View File
@@ -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
+244
View File
@@ -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" "<metadata>" "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" "<versions>" "Should have <versions> 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 <versions> element" test_neoforge_metadata_has_versions
run_test "Metadata has <latest> version" test_neoforge_metadata_has_latest
run_test "Metadata has <release> 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
+271
View File
@@ -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
+281
View File
@@ -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:-<empty>}"
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
}