Files
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

272 lines
9.3 KiB
Bash
Executable File

#!/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