Compare commits

20 Commits
Author SHA1 Message Date
jeremy 5688b30892 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
2026-08-21 12:49:27 -04:00
jeremy a6f610de28 fix: derive VER_LOADER from existing symlink on restart 2026-06-17 22:34:29 -04:00
jeremy 6e13a0aa15 refactor: modular init, add bedrock loader, dynamic java/healthcheck
Build dev image / build (push) Failing after 18m12s
- Split monolithic init into modular lib files under scripts/lib/
  (common.sh, java.sh, server.sh, loaders/{vanilla,paper,fabric,forge,neoforge}.sh)
- Add bedrock loader: downloads from Minecraft Services API,
  runs native binary with LD_LIBRARY_PATH, skips Java setup
- Add auto-detection for JAVA_VERSION from Mojang version manifest
  (MC 1.20.5+ -> temurin@21, 1.17-1.20.4 -> temurin@17,
   1.17 -> temurin@16, pre-1.17 -> temurin@8)
- Fix NEW_FORGE unbound variable when non-Forge loaders run
- Fix Fabric loader jq filter (-s slurp removed, null string check fixed)
- Fix Forge test detection (recursive find for forge-*-server.jar)
- Dynamic healthcheck: nc -z TCP 25565 for Java, nc -zu UDP 19132 for bedrock
- Add .gitea/workflows/build-dev.yml for dev branch CI builds
- Base image: debian:bookworm-slim (from ubuntu:noble)
- Add unzip, netcat-openbsd to Docker image
- Switch to shebangs, set -euo pipefail throughout
2026-06-17 18:41:26 -04:00
jeremy 2d46f91c60 adjust docker compose with new standards for Xms and Xmx
Docker Build and Push / build (push) Successful in 1m11s
2025-07-25 21:51:06 -04:00
jeremy 0df24cb64a neoforge modification to differentiate between new and old 2025-07-25 21:50:24 -04:00
jeremy f871915c08 incorrect variable fix
Docker Build and Push / build (push) Successful in 1m5s
2025-07-23 13:24:30 -04:00
jeremy 9c8bc802a8 correct launch parameters for neoforge
Docker Build and Push / build (push) Successful in 1m12s
2025-07-23 13:21:11 -04:00
jeremy d78cfd39c1 bluff called, touche
Docker Build and Push / build (push) Successful in 1m5s
2025-07-23 11:39:03 -04:00
jeremy 2223ea9969 THIS IS THE LAST TIME, YOU HEAR ME?
Docker Build and Push / build (push) Successful in 1m5s
2025-07-23 11:35:35 -04:00
jeremy 527dd5b4e7 this is what happens when you push a hack
Docker Build and Push / build (push) Successful in 1m1s
2025-07-23 11:33:14 -04:00
jeremy 4c155584af run.sh temp hack
Docker Build and Push / build (push) Successful in 1m7s
2025-07-23 11:29:03 -04:00
jeremy 5383662f9f take out removal of needed files
Docker Build and Push / build (push) Successful in 1m8s
2025-07-22 15:05:57 -04:00
jeremy feca6cfa83 Fix forge and implement NeoForge
Docker Build and Push / build (push) Successful in 2m36s
2025-07-22 12:32:13 -04:00
jeremy b0c1b07499 Loader version option
Docker Build and Push / build (push) Successful in 2m25s
2025-06-18 07:52:11 -04:00
jeremy 744bd61ee6 fixed inverted logic
Docker Build and Push / build (push) Successful in 3m11s
2025-04-11 16:02:19 -04:00
jeremy ec9c48ac68 remove references to old script
Docker Build and Push / build (push) Successful in 1m5s
2025-03-30 00:46:48 -04:00
jeremy 8dafd475ba remove old script, moved to init 2025-03-30 00:46:21 -04:00
jeremy f325f54ee9 add comment for server choice 2025-03-30 00:45:55 -04:00
jeremy b425ab200a Move everything into single init 2025-03-30 00:45:34 -04:00
jeremy cff62979ff change Eula handling, disable server start
Docker Build and Push / build (push) Successful in 1m6s
2025-03-29 06:11:18 -04:00
27 changed files with 3037 additions and 192 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
+27
View File
@@ -0,0 +1,27 @@
name: Build dev image
on:
push:
branches: [dev]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to local Gitea registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY }}
username: ${{ vars.ACTIONS_USER }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Docker Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:dev
cache-from: type=gha
cache-to: type=gha,mode=max
+2
View File
@@ -25,3 +25,5 @@ jobs:
context: .
push: true
tags: ${{ vars.REGISTRY }}/${{ gitea.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
+16
View File
@@ -1 +1,17 @@
# Runtime data
data/
# Editor/IDE
*.swp
*.swo
*~
.vscode/
.idea/
*.iml
# OS
.DS_Store
Thumbs.db
# Test artifacts
*.log
+24 -13
View File
@@ -1,36 +1,47 @@
FROM ubuntu:noble
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"]
# install prerequisites
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
jq \
libxml2-utils \
sudo && \
apt-get autoremove && apt-get clean && \
sudo \
tini \
unzip \
netcat-openbsd && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Remove default ubuntu user
RUN userdel -r ubuntu && \
useradd --create-home -u 1000 minecraft && \
RUN groupadd -g ${GID} minecraft && \
useradd --create-home -u ${UID} -g ${GID} minecraft && \
echo 'minecraft ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && \
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/init /
COPY ./scripts/mc /usr/local/bin
RUN chmod +x /init && chmod +x /usr/local/bin/mc
COPY ./scripts/ /scripts/
RUN chmod +x /scripts/init
EXPOSE 25565/tcp 19132/udp 19133/udp
STOPSIGNAL SIGTERM
USER minecraft
CMD ["/init"]
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"]
+215 -1
View File
@@ -1 +1,215 @@
TODO
# Dockercraft
A Docker image for running Minecraft servers with support for multiple loaders.
## 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:
minecraft:
image: git.jeremymcclure.com/jeremy/dockercraft:latest
container_name: minecraft
stdin_open: true
tty: true
environment:
- EULA=true
- MC_VERSION=1.21.1
- MC_LOADER=neoforge
- MC_LOADER_VERSION=21.1.185
- JAVA_VERSION=temurin@21
- XMS=2G
- XMX=4G
- PUID=1000
- PGID=1000
volumes:
- ./data:/data
ports:
- 25565:25565
restart: unless-stopped
```
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `EULA` | — | **Required.** Set to `true` to accept the Minecraft EULA |
| `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 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 |
| `XMX` | `4G` | Maximum Java heap size |
| `ADD_ARGS` | — | Additional Java arguments |
| `PUID` | `1000` | User ID for /data ownership |
| `PGID` | `1000` | Group ID for /data ownership |
## Supported Loaders
| 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.
+58 -6
View File
@@ -1,26 +1,78 @@
services:
dockercraft:
image: git.jeremymcclure.com/jeremy/dockercraft:latest
# Use :dev to test builds from the dev branch
#image: git.jeremymcclure.com/jeremy/dockercraft:dev
container_name: dockercraft
stdin_open: true
tty: true
environment:
- MC_LOADER=paper
- MC_VERSION=1.21.4
- JAVA_VERSION=temurin@21
- EULA=true
- MC_VERSION=1.21.1
# Server loader type: paper, fabric, forge, neoforge, or bedrock
- MC_LOADER=neoforge
# --- 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=
- FORGE_VERSION=
- NEOFORGE_VERSION=
- PAPER_BUILD=
#
# MC_LOADER_VERSION is a fallback for any loader that doesn't have
# its own var set. You usually don't need this.
- MC_LOADER_VERSION=
# -------------------------------------------------------------------
# For bedrock, MC_VERSION is the Bedrock server version (e.g. 1.21.50)
# and no Java is needed.
#
# --- Java Version --------------------------------------------------
# If unset, auto-detected from Mojang version manifest:
# MC 1.20.5+ -> temurin@21, 1.17-1.20.4 -> temurin@17,
# 1.17 -> temurin@16, pre-1.17 -> temurin@8
# Set explicitly to pin a version. Uses Jabba
# (https://github.com/Jabba-Team/jabba) — supports temurin, zulu,
# adopt, openjdk, graalvm, and more.
#- JAVA_VERSION=temurin@21
# -------------------------------------------------------------------
- JAR=server.jar
- MEMORY=2G
- XMS=2G
- XMX=4G
- ADD_ARGS=
- RESTART=true
- PUID=1000
- PGID=1000
build:
dockerfile: Dockerfile
context: .
volumes:
- ./data:/data
ports:
# Java (vanilla, paper, fabric, forge, neoforge): TCP 25565
# Bedrock: UDP 19132 (and UDP 19133 for IPv6)
# Expose the port matching your MC_LOADER:
- 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:
memory: 2G
memory: 4G
restart: unless-stopped
Regular → Executable
+73 -10
View File
@@ -1,18 +1,81 @@
#!/bin/env bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ---------- source helpers first (so log/die are available) ----------------
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib/common.sh"
# ---------- environment variables ------------------------------------------
MC_VERSION="${MC_VERSION:-}"
MC_LOADER="${MC_LOADER:-}"
MC_LOADER_VERSION="${MC_LOADER_VERSION:-}"
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:-}"
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}"
setperms() {
echo "Setting permissions to UID:${PUID} and GID:${PGID}"
sudo usermod -u ${PUID} minecraft >/dev/null 2>&1
sudo groupmod -g ${PGID} minecraft >/dev/null 2>&1
sudo chown -R ${PUID}:${PGID} /data >/dev/null 2>&1
echo "DONE"
sleep 2
}
PATH="${JAVA_HOME}/bin:$PATH"
# ---------- validate required vars early -----------------------------------
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
mc
if [[ "$MC_LOADER" != "bedrock" ]]; then
javaCheck
fi
serverSelect
if [[ "$MC_LOADER" != "bedrock" ]]; then
buildCommand
fi
serverStart
+71
View File
@@ -0,0 +1,71 @@
# ---------- helpers --------------------------------------------------------
log() {
echo -e "[dockercraft] $*"
}
warn() {
echo -e "[dockercraft] WARNING: $*" >&2
}
die() {
echo -e "[dockercraft] ERROR: $*" >&2
sleep 2
exit 1
}
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
die "EULA not accepted. Set EULA=true to accept the Minecraft EULA (https://aka.ms/MinecraftEULA)"
fi
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
else
NEW_FORGE=false
fi
}
serverInfoWrite() {
cat <<EOF > /data/server/server.info
MC_VERSION=${MC_VERSION}
MC_LOADER=${MC_LOADER}
MC_LOADER_VERSION=${MC_LOADER_VERSION}
JAVA_VERSION=${JAVA_VERSION:-"N/A"}
JAR=${JAR:-"N/A"}
XMS=${XMS:-"N/A"}
XMX=${XMX:-"N/A"}
ADD_ARGS=${ADD_ARGS:-"NONE"}
PUID=${PUID}
PGID=${PGID}
EOF
}
+64
View File
@@ -0,0 +1,64 @@
javaVersionDetect() {
if [[ -n "${JAVA_VERSION:-}" ]]; then
return
fi
log "Auto-detecting Java version from Mojang 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
warn "Could not fetch Mojang manifest, defaulting to temurin@21"
JAVA_VERSION="temurin@21"
return
fi
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 --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
local JAVA_MAJOR
JAVA_MAJOR=$(echo "$VERSION_DATA" | jq -r ".javaVersion.majorVersion // empty")
if [[ -z "$JAVA_MAJOR" ]]; then
JAVA_VERSION="temurin@8"
else
JAVA_VERSION="temurin@${JAVA_MAJOR}"
fi
log "Detected Java version: ${JAVA_VERSION}"
}
javaCheck() {
javaVersionDetect
JAVA_HOME="/data/java/${JAVA_VERSION}"
if [[ -f "$HOME/.jabba/jabba.sh" ]]; then
# shellcheck disable=SC1090
source "$HOME/.jabba/jabba.sh"
else
die "Jabba not found at ~/.jabba/jabba.sh — installation may have failed"
fi
if [[ ! -f "/data/java/${JAVA_VERSION}/bin/java" ]]; then
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"
JAVA="java"
}
+49
View File
@@ -0,0 +1,49 @@
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"
_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)" --connect-timeout 10 --max-time 30 \
"$BEDROCK_API_URL" |
jq -r ".result.links[] | select(.downloadType == \"${platform}\") | .downloadUrl // empty")
if [[ -z "$DL_URL" ]]; then
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
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)')
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() {
SERVER_TYPE="Bedrock Server"
_bedrockDownload "serverBedrockLinux" "${MC_VERSION:-}"
}
getBedrockPreview() {
SERVER_TYPE="Bedrock Server (Preview)"
_bedrockDownload "serverBedrockPreviewLinux" "${MC_VERSION:-}"
}
+33
View File
@@ -0,0 +1,33 @@
getFabric() {
SERVER_TYPE="Fabric Minecraft"
if [[ -z "${MC_VERSION}" ]]; then
die "MC_VERSION is required for Fabric loader"
fi
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}}"
local DL_URL
DL_URL="https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${VER_LOADER}/${LATEST_INSTALLER}/server/jar"
log "Downloading Fabric (loader ${VER_LOADER}, installer ${LATEST_INSTALLER})..."
curl -fL -o server.jar "${DL_URL}"
log "Fabric ${MC_VERSION} (loader ${VER_LOADER}) downloaded."
}
+45
View File
@@ -0,0 +1,45 @@
getForge() {
SERVER_TYPE="Forge Server"
if [[ -z "${MC_VERSION}" ]]; then
die "MC_VERSION is required for Forge loader"
fi
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 [[ -z "$LATEST_BUILD" || "$LATEST_BUILD" == "null" ]]; then
die "No Forge version found for MC ${MC_VERSION}"
fi
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
}
+55
View File
@@ -0,0 +1,55 @@
getNeoForge() {
SERVER_TYPE="NeoForge Server"
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
if [[ -z "$VER_LOADER" ]]; then
die "Could not auto-detect NeoForge version for MC ${MC_VERSION}"
fi
fi
DL_URL="https://maven.neoforged.net/releases/net/neoforged/neoforge/${VER_LOADER}/neoforge-${VER_LOADER}-installer.jar"
log "Downloading NeoForge ${VER_LOADER} installer..."
rm -f server.jar
curl -fL -o "neoforge-${VER_LOADER}-installer.jar" "${DL_URL}"
log "Running NeoForge installer..."
$JAVA -jar "neoforge-${VER_LOADER}-installer.jar" --installServer --server.jar
mv server.jar "neoforge-${VER_LOADER}.jar"
ln -sf "neoforge-${VER_LOADER}.jar" server.jar
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."
}
+59
View File
@@ -0,0 +1,59 @@
getPaper() {
SERVER_TYPE="Paper Minecraft"
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 [[ -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
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."
}
+38
View File
@@ -0,0 +1,38 @@
getVanilla() {
SERVER_TYPE="Vanilla Minecraft"
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."
}
+103
View File
@@ -0,0 +1,103 @@
serverSelect() {
mkdir -p /data/server
cd /data/server || die "Cannot access /data/server"
if [[ -f "server.jar" ]] || [[ -f "run.sh" ]] || [[ -f "bedrock_server" ]]; then
log "Found existing server files, skipping download."
else
case "$MC_LOADER" in
vanilla)
getVanilla
;;
paper)
getPaper
;;
fabric)
getFabric
;;
forge)
getForge
;;
neoforge)
getNeoForge
;;
bedrock)
getBedrock
;;
*)
die "Server type '${MC_LOADER}' is not recognized. Valid: vanilla, paper, fabric, forge, neoforge, bedrock"
;;
esac
fi
}
buildCommand() {
JAVA_ARGS="-Xms${XMS} -Xmx${XMX}"
if [[ -n "${ADD_ARGS}" ]]; then
RUN_STRING="${ADD_ARGS} ${JAVA_ARGS} -jar ${JAR} nogui"
else
RUN_STRING="${JAVA_ARGS} -jar ${JAR} nogui"
fi
}
detectForgeVersion() {
# Detect the forge/neoforge version from the installed jar names
VER_LOADER="${VER_LOADER:-}"
if [[ -n "$VER_LOADER" ]]; then
return
fi
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
}
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
}
-160
View File
@@ -1,160 +0,0 @@
#!/bin/env bash
MC_LOADER="${MC_LOADER:-"paper"}"
MC_VERSION=${MC_VERSION:-"1.21.4"}
JAVA_VERSION=${JAVA_VERSION:-"temurin@21"}
JAR=${JAR:-"server.jar"}
MEM=${MEMORY:-2G}
ADD_ARGS=${ADD_ARGS:-""}
RESTART=${RESTART:-"true"}
crash() {
sleep 5
exit 1
}
setEula() {
if [[ ! -s "eula.txt" ]]; then
echo "#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA)." >eula.txt
echo "eula=true" >>eula.txt
fi
}
javaCheck() {
JAVA_HOME=/data/java/${JAVA_VERSION}
if [[ -f "$HOME/.jabba/jabba.sh" ]]; then
source $HOME/.jabba/jabba.sh
else
echo "Something went wrong with locating jabba(the java installer)"
exit 0
fi
if [[ ! -n "${JAVA_VERSION}" ]]; then
echo "NO JAVA_VERSION ENVIRONMENT VARIABLE SET SET IT"
crash
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}
fi
PATH=${JAVA_HOME}/bin:$PATH
JAVA="java"
}
getPaper() {
LATEST_BUILD=$(curl -s https://api.papermc.io/v2/projects/${MC_LOADER}/versions/${MC_VERSION}/builds |
jq -r '.builds | map(select(.channel == "default") | .build) | .[-1]')
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}"
# Download the latest Paper version
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 :("
fi
}
getFabric() {
LATEST_BUILD=$(curl -s https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION} | jq -s '.[] .[0] .loader .version' | tr -d '"')
LATEST_INSTALLER=$(curl -s https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml | xmllint --xpath "/metadata/versioning/latest/text()" -)
DL_URL=https://meta.fabricmc.net/v2/versions/loader/${MC_VERSION}/${LATEST_BUILD}/${LATEST_INSTALLER}/server/jar
if [ "$LATEST_BUILD" != "null" ]; then
echo "$DL_URL"
curl -o server.jar ${DL_URL}
else
echo "TODO ERROR TEXT"
fi
}
getForge() {
LATEST_BUILD=$(curl -s https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json | jq -r ".promos[\"${MC_VERSION}-latest\"]")
DL_BUILD=${MC_VERSION}-${LATEST_BUILD}
DL_URL=https://maven.minecraftforge.net/net/minecraftforge/forge/$DL_BUILD/forge-$DL_BUILD-installer.jar
if [ "$LATEST_BUILD" != "null" ]; then
echo "$DL_URL"
curl -o forge-install.jar ${DL_URL}
echo "$JAVA -jar forge-install.jar --installServer"
$JAVA -jar forge-install.jar --installServer
ln -s forge-${MC_VERSION}-${LATEST_BUILD}-shim.jar server.jar
touch server.properties
rm -f user_jvm_args.txt
rm -f forge-install.jar
rm -f forge-install.jar.log
rm -f README.txt
rm -f run.bat
rm -f run.sh
else
echo "TODO ERROR TEXT"
fi
}
serverSelect() {
mkdir -p /data/server
cd /data/server
if [[ ! -f server.jar ]]; then
case $MC_LOADER in
vanilla)
getVanilla
;;
paper)
getPaper
;;
fabric)
getFabric
;;
forge)
getForge
;;
*)
echo "Server of type ${MC_LOADER} is not recognized."
crash
;;
esac
fi
}
buildCommand() {
JAVA_ARGS="-Xms${MEM} -Xmx${MEM}"
if [[ -z ${ADD_ARGS+x} ]]; then
RUN_STRING="${ADD_ARGS} ${JAVA_ARGS} -jar ${JAR} nogui"
else
RUN_STRING="${JAVA_ARGS} -jar ${JAR} nogui"
fi
}
runServer() {
setEula
while true; do
if [[ -f $JAR ]]; then
buildCommand
echo "${JAVA} ${RUN_STRING}"
$JAVA ${RUN_STRING}
else
echo "${JAR} not found."
fi
if [[ "${RESTART}" != "true" ]]; then
echo "Exiting..."
exit 0
fi
echo -e "\n${RD}Automatically restarting server in 5 seconds. Press CTRL + C to abort and exit.${NC}"
sleep 5
done
}
# ------------------------------------------------------------------------------- #
javaCheck
serverSelect
runServer
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Test Minecraft server loader integrations in isolated Docker containers.
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:
$(basename "$0") Test all loaders (latest MC)
$(basename "$0") -m 1.21.1 Test all loaders on MC 1.21.1
$(basename "$0") -m 1.21.1 -l fabric Test Fabric on MC 1.21.1
$(basename "$0") -m 1.21.1 -l fabric -v 0.19.3 Test Fabric 0.19.3 on MC 1.21.1
Loaders: vanilla paper fabric forge neoforge bedrock
EOF
exit 0
}
detect_mc_version() {
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
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
-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
if [[ -z "$MC_VERSION" ]]; then
echo "Detecting latest Minecraft version..."
MC_VERSION=$(detect_mc_version)
echo " -> ${MC_VERSION}"
fi
if [[ -z "$LOADER" ]]; then
tests=("vanilla" "paper" "fabric" "forge" "neoforge" "bedrock")
else
tests=("$LOADER")
fi
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() {
local name="$1"
shift
echo "=== Testing: ${name} ==="
cleanup "$name"
# 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 "dc-test-vol-${name}:/data" \
"${IMAGE}" 2>&1); then
local waited=0
local jar_found=false
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
sleep 3
waited=$((waited + 3))
echo -n "."
done
echo ""
if $jar_found; 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)"
fi
PASS=$((PASS + 1))
else
echo ""
echo " FAIL: server binary not found after ${TIMEOUT}s"
echo " --- container logs ---"
docker logs "dc-test-${name}" 2>&1 | tail -50 || true
echo " ---------------------"
FAIL=$((FAIL + 1))
fi
else
echo ""
echo " FAIL: container failed to start"
echo " ${CONTAINER_ID}"
FAIL=$((FAIL + 1))
fi
cleanup "$name"
}
echo "Building image..."
DOCKER_BUILDKIT=0 docker build -t "${IMAGE}" "${PROJECT_ROOT}"
for t in "${tests[@]}"; do
case $t in
vanilla)
run_test "vanilla" -e MC_LOADER=vanilla
;;
paper)
run_test "paper" -e MC_LOADER=paper
;;
fabric)
run_test "fabric" \
-e MC_LOADER=fabric \
-e FABRIC_LOADER_VERSION="${LOADER_VERSION}"
;;
forge)
run_test "forge" \
-e MC_LOADER=forge \
-e FORGE_VERSION="${LOADER_VERSION}"
;;
neoforge)
run_test "neoforge" \
-e MC_LOADER=neoforge \
-e NEOFORGE_VERSION="${LOADER_VERSION}"
;;
bedrock)
run_test "bedrock" \
-e MC_LOADER=bedrock \
-e MC_VERSION=
;;
*)
echo "Unknown loader: ${t}"
echo "Valid: vanilla paper fabric forge neoforge bedrock"
exit 1
;;
esac
done
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ $FAIL -eq 0 ]]
+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
}