Files
cleanup-nuget/tests/entrypoint.sh
T

474 lines
15 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENTRYPOINT="${ROOT_DIR}/entrypoint.sh"
TEST_TMPDIR="$(mktemp -d)"
TEST_BIN_DIR="${TEST_TMPDIR}/bin"
CURL_MOCK_ROUTES="${TEST_TMPDIR}/curl.routes"
trap 'rm -rf -- "${TEST_TMPDIR}"' EXIT
mkdir -p "${TEST_BIN_DIR}"
: > "${CURL_MOCK_ROUTES}"
export PATH="${TEST_BIN_DIR}:${PATH}"
export CURL_MOCK_ROUTES
cat > "${TEST_BIN_DIR}/jq" <<'EOF'
#!/usr/bin/env python3
import json
import sys
import urllib.parse
def load_json_text(text):
text = text.strip()
if not text:
return None
return json.loads(text)
def read_json_file(path):
if path == "-":
return load_json_text(sys.stdin.read())
with open(path, "r", encoding="utf-8") as fh:
return load_json_text(fh.read())
args = sys.argv[1:]
expr = None
files = []
variables = {}
raw = False
compact = False
slurp = False
null_input = False
i = 0
while i < len(args):
arg = args[i]
if arg.startswith("-") and len(arg) > 1 and set(arg[1:]).issubset({"r", "n", "c", "s"}):
raw = raw or ("r" in arg[1:])
null_input = null_input or ("n" in arg[1:])
slurp = slurp or ("s" in arg[1:])
compact = compact or ("c" in arg[1:])
i += 1
continue
if arg == "--arg":
variables[args[i + 1]] = args[i + 2]
i += 3
continue
if arg == "--argjson":
variables[args[i + 1]] = json.loads(args[i + 2])
i += 3
continue
if arg.startswith("-"):
i += 1
continue
expr = arg
files = args[i + 1 :]
break
if expr is None:
raise SystemExit("jq mock requires an expression")
if slurp:
if files:
docs = [read_json_file(path) for path in files]
elif null_input:
docs = []
else:
docs = [load_json_text(sys.stdin.read())]
doc = docs
elif files:
doc = read_json_file(files[0])
else:
doc = None if null_input else load_json_text(sys.stdin.read())
def emit(text):
sys.stdout.write(text)
def emit_json(value):
sys.stdout.write(json.dumps(value, separators=(",", ":")))
if expr == "$value|@uri":
emit(urllib.parse.quote(str(variables["value"]), safe="-._~"))
elif expr == "length":
emit(str(len(doc)))
elif expr == "sort_by(.created_at, .version)[] | [.version, .created_at] | @tsv":
for item in sorted(doc, key=lambda entry: (entry.get("created_at", ""), entry.get("version", ""))):
emit(f"{item.get('version', '')}\t{item.get('created_at', '')}\n")
elif expr == "sort_by(.created_at, .version) | .[0:(length - $keep)]":
keep = int(variables["keep"])
items = sorted(doc, key=lambda entry: (entry.get("created_at", ""), entry.get("version", "")))
emit_json(items[: max(len(items) - keep, 0)])
elif expr == ".[] | [.name, .version, .created_at] | @tsv":
for item in doc:
emit(f"{item.get('name', '')}\t{item.get('version', '')}\t{item.get('created_at', '')}\n")
elif expr == ".[0] + .[1]":
if len(doc) < 2:
raise SystemExit("jq mock expected two inputs for slurp merge")
emit_json(doc[0] + doc[1])
else:
raise SystemExit(f"jq mock does not support expression: {expr}")
EOF
cat > "${TEST_BIN_DIR}/curl" <<'EOF'
#!/bin/bash
set -euo pipefail
method="GET"
headers_file=""
body_file=""
url=""
while (($#)); do
case "$1" in
-s|-S|-sS)
shift
;;
-H)
shift 2
;;
-X)
method="$2"
shift 2
;;
-D)
headers_file="$2"
shift 2
;;
-o)
body_file="$2"
shift 2
;;
-w)
shift 2
;;
--)
shift
break
;;
-*)
shift
;;
*)
url="$1"
shift
;;
esac
done
if [[ -z "${headers_file}" || -z "${body_file}" || -z "${url}" ]]; then
echo "curl mock missing required arguments" >&2
exit 1
fi
if [[ -n "${CURL_FAIL_MATCH:-}" && "${method}|${url}" == "${CURL_FAIL_MATCH}" ]]; then
echo "curl mock forced failure for ${method} ${url}" >&2
exit 7
fi
route_line="$(
awk -F $'\t' -v method="${method}" -v url="${url}" '$1 == method && $2 == url { print; exit }' "${CURL_MOCK_ROUTES}"
)"
if [[ -z "${route_line}" ]]; then
echo "curl mock missing route for ${method} ${url}" >&2
exit 1
fi
IFS=$'\t' read -r _ _ code status request_id body <<< "${route_line}"
printf 'HTTP/1.1 %s %s\r\n' "${code}" "${status}" > "${headers_file}"
if [[ -n "${request_id}" ]]; then
printf 'x-gitea-request-id: %s\r\n' "${request_id}" >> "${headers_file}"
fi
printf '\r\n' >> "${headers_file}"
printf '%s' "${body}" > "${body_file}"
printf '%s' "${code}"
EOF
chmod +x "${TEST_BIN_DIR}/jq" "${TEST_BIN_DIR}/curl"
source "${ENTRYPOINT}"
CAPTURE_STDOUT=""
CAPTURE_STDERR=""
CAPTURE_STATUS=0
assert_eq() {
local expected="$1"
local actual="$2"
local message="${3:-values differ}"
if [[ "${expected}" != "${actual}" ]]; then
printf 'ASSERTION FAILED: %s\nexpected: %q\nactual: %q\n' "${message}" "${expected}" "${actual}" >&2
return 1
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local message="${3:-substring missing}"
if [[ "${haystack}" != *"${needle}"* ]]; then
printf 'ASSERTION FAILED: %s\nmissing: %s\ntext: %q\n' "${message}" "${needle}" "${haystack}" >&2
return 1
fi
}
capture_call() {
local stdout_file stderr_file
stdout_file="$(mktemp)"
stderr_file="$(mktemp)"
set +e
( "$@" ) > "${stdout_file}" 2> "${stderr_file}"
CAPTURE_STATUS=$?
set -e
CAPTURE_STDOUT="$(<"${stdout_file}")"
CAPTURE_STDERR="$(<"${stderr_file}")"
rm -f -- "${stdout_file}" "${stderr_file}"
}
reset_env() {
unset INPUT_KEEP_COUNT INPUT_PACKAGE_NAMES GITEA_SERVER_URL GITEA_REPOSITORY RUNNER_TOKEN PAGE_LIMIT CURL_FAIL_MATCH
: > "${CURL_MOCK_ROUTES}"
}
add_route() {
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" >> "${CURL_MOCK_ROUTES}"
}
run_test() {
local name="$1"
shift
if "$@"; then
printf 'ok - %s\n' "${name}"
else
printf 'not ok - %s\n' "${name}" >&2
return 1
fi
}
test_trim() {
capture_call trim ""
assert_eq "" "${CAPTURE_STDOUT}" "trim empty string"
capture_call trim " "
assert_eq "" "${CAPTURE_STDOUT}" "trim whitespace only"
capture_call trim " abc "
assert_eq "abc" "${CAPTURE_STDOUT}" "trim trims edges"
capture_call trim "a b"
assert_eq "a b" "${CAPTURE_STDOUT}" "trim keeps inner whitespace"
capture_call trim "abc"
assert_eq "abc" "${CAPTURE_STDOUT}" "trim keeps plain values"
}
test_url_encode() {
capture_call url_encode "a b/c?x=1&y=two"
assert_eq "a%20b%2Fc%3Fx%3D1%26y%3Dtwo" "${CAPTURE_STDOUT}" "url_encode encodes reserved characters"
capture_call url_encode "already%20encoded"
assert_eq "already%2520encoded" "${CAPTURE_STDOUT}" "url_encode encodes percent signs"
}
test_resolve_keep_count() {
reset_env
capture_call resolve_keep_count
assert_eq "2" "${CAPTURE_STDOUT}" "resolve_keep_count default"
INPUT_KEEP_COUNT=" 5 "
capture_call resolve_keep_count
assert_eq "5" "${CAPTURE_STDOUT}" "resolve_keep_count trims input"
INPUT_KEEP_COUNT="0"
capture_call resolve_keep_count
assert_eq "0" "${CAPTURE_STDOUT}" "resolve_keep_count accepts zero"
}
test_resolve_keep_count_invalid() {
local value
for value in abc -1 1.5; do
reset_env
INPUT_KEEP_COUNT="${value}"
capture_call resolve_keep_count
assert_eq "1" "${CAPTURE_STATUS}" "resolve_keep_count fails for ${value}"
assert_contains "${CAPTURE_STDERR}" "ERROR: Invalid keep_count: ${value}" "resolve_keep_count error message"
done
}
test_resolve_package_names() {
reset_env
INPUT_PACKAGE_NAMES=$' pkg-a , pkg-b\npkg-a,,pkg-c '
capture_call resolve_package_names
assert_eq $'pkg-a\npkg-b\npkg-c' "${CAPTURE_STDOUT}" "resolve_package_names trims, dedupes, and keeps order"
}
test_resolve_package_names_missing() {
reset_env
capture_call resolve_package_names
assert_eq "1" "${CAPTURE_STATUS}" "resolve_package_names fails when empty"
assert_contains "${CAPTURE_STDERR}" "ERROR: Missing PACKAGE_NAMES" "resolve_package_names missing message"
}
test_parse_repo_context() {
capture_call parse_repo_context " org/project-name "
assert_eq $'org\tproject-name' "${CAPTURE_STDOUT}" "parse_repo_context trims inputs"
capture_call parse_repo_context "owner/repo/sub"
assert_eq "1" "${CAPTURE_STATUS}" "parse_repo_context rejects nested paths"
assert_contains "${CAPTURE_STDERR}" "ERROR: Invalid GITEA_REPOSITORY: owner/repo/sub" "parse_repo_context error message"
}
test_api_request() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
add_route GET "https://gitea.example/api/v1/test" 200 OK req-123 '{"ok":true}'
local body_file headers_file
body_file="$(mktemp)"
headers_file="$(mktemp)"
capture_call api_request token GET "/api/v1/test" "${body_file}" "${headers_file}"
assert_eq $'200\t200 OK\treq-123' "${CAPTURE_STDOUT}" "api_request metadata"
assert_contains "${CAPTURE_STDERR}" "GET /api/v1/test -> 200 OK request_id=req-123" "api_request log line"
assert_eq '{"ok":true}' "$(cat "${body_file}")" "api_request writes body"
assert_contains "$(cat "${headers_file}")" "x-gitea-request-id: req-123" "api_request writes headers"
export CURL_FAIL_MATCH="GET|https://gitea.example/api/v1/test"
capture_call api_request token GET "/api/v1/test" "${body_file}" "${headers_file}"
assert_eq "1" "${CAPTURE_STATUS}" "api_request propagates curl failure"
assert_contains "${CAPTURE_STDERR}" "ERROR: Request failed: GET /api/v1/test" "api_request failure message"
unset CURL_FAIL_MATCH
}
test_fetch_package_versions_404() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/missing?page=1&limit=100" 404 "Not Found" req-404 '[]'
capture_call fetch_package_versions acme missing token
assert_eq "[]" "${CAPTURE_STDOUT}" "fetch_package_versions returns empty array for 404"
}
test_fetch_package_versions_paginated() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
PAGE_LIMIT=2
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-a?page=1&limit=2" 200 OK req-1 '[{"name":"pkg-a","version":"1.0.0","created_at":"2024-01-01T00:00:00Z"},{"name":"pkg-a","version":"1.1.0","created_at":"2024-02-01T00:00:00Z"}]'
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-a?page=2&limit=2" 200 OK req-2 '[{"name":"pkg-a","version":"1.2.0","created_at":"2024-03-01T00:00:00Z"}]'
capture_call fetch_package_versions acme pkg-a token
assert_eq '[{"name":"pkg-a","version":"1.0.0","created_at":"2024-01-01T00:00:00Z"},{"name":"pkg-a","version":"1.1.0","created_at":"2024-02-01T00:00:00Z"},{"name":"pkg-a","version":"1.2.0","created_at":"2024-03-01T00:00:00Z"}]' "${CAPTURE_STDOUT}" "fetch_package_versions paginates and merges"
}
test_collect_package_candidates() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
local candidate_file
candidate_file="$(mktemp)"
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-a?page=1&limit=100" 200 OK req-1 '[{"name":"pkg-a","version":"1.0.0","created_at":"2024-01-01T00:00:00Z"},{"name":"pkg-a","version":"2.0.0","created_at":"2024-02-01T00:00:00Z"},{"name":"pkg-a","version":"3.0.0","created_at":"2024-03-01T00:00:00Z"}]'
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-b?page=1&limit=100" 200 OK req-2 '[{"name":"pkg-b","version":"9.9.9","created_at":"2024-04-01T00:00:00Z"}]'
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-missing?page=1&limit=100" 404 "Not Found" req-3 '[]'
capture_call collect_package_candidates acme 2 "${candidate_file}" token pkg-a pkg-b pkg-missing
assert_eq $'2\t4\t3\t1' "${CAPTURE_STDOUT}" "collect_package_candidates summary"
assert_eq $'pkg-a\t1.0.0\t2024-01-01T00:00:00Z' "$(cat "${candidate_file}")" "collect_package_candidates chooses oldest version"
assert_contains "${CAPTURE_STDERR}" "No versions found for package pkg-missing" "collect_package_candidates logs missing packages"
}
test_process_candidates_empty() {
reset_env
local candidate_file
candidate_file="$(mktemp)"
capture_call process_candidates acme "${candidate_file}" 0 0 0 0 token
assert_contains "${CAPTURE_STDERR}" "No delete candidates found" "process_candidates empty file"
assert_contains "${CAPTURE_STDERR}" "Summary: packages=0 versions=0 kept=0 candidates=0 deleted=0 errors=0" "process_candidates empty summary"
}
test_process_candidates() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
local candidate_file
candidate_file="$(mktemp)"
printf 'pkg-a\t1.0.0\t2024-01-01T00:00:00Z\npkg-b\t2.0.0\t2024-02-01T00:00:00Z\n' > "${candidate_file}"
add_route DELETE "https://gitea.example/api/v1/packages/acme/nuget/pkg-a/1.0.0" 204 "No Content" del-1 ''
add_route DELETE "https://gitea.example/api/v1/packages/acme/nuget/pkg-b/2.0.0" 500 "Internal Server Error" del-2 '{"error":"boom"}'
capture_call process_candidates acme "${candidate_file}" 2 4 3 2 token
assert_contains "${CAPTURE_STDERR}" "Deleted package pkg-a version 1.0.0 -> 204 No Content" "process_candidates success path"
assert_contains "${CAPTURE_STDERR}" "ERROR: DELETE package pkg-b version 2.0.0 -> 500 Internal Server Error request_id=del-2" "process_candidates failure path"
assert_contains "${CAPTURE_STDERR}" "Summary: packages=2 versions=4 kept=3 candidates=2 deleted=1 errors=1" "process_candidates summary"
}
test_main_integration() {
reset_env
GITEA_SERVER_URL="https://gitea.example"
GITEA_REPOSITORY="acme/repo"
RUNNER_TOKEN="token"
INPUT_KEEP_COUNT="2"
INPUT_PACKAGE_NAMES="pkg-a,pkg-b"
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-a?page=1&limit=100" 200 OK req-a '[{"name":"pkg-a","version":"1.0.0","created_at":"2024-01-01T00:00:00Z"},{"name":"pkg-a","version":"2.0.0","created_at":"2024-02-01T00:00:00Z"},{"name":"pkg-a","version":"3.0.0","created_at":"2024-03-01T00:00:00Z"}]'
add_route GET "https://gitea.example/api/v1/packages/acme/nuget/pkg-b?page=1&limit=100" 200 OK req-b '[{"name":"pkg-b","version":"9.9.9","created_at":"2024-04-01T00:00:00Z"}]'
add_route DELETE "https://gitea.example/api/v1/packages/acme/nuget/pkg-a/1.0.0" 204 "No Content" del-a ''
capture_call main
assert_contains "${CAPTURE_STDERR}" "keep_count=2" "main logs keep count"
assert_contains "${CAPTURE_STDERR}" "package_names=pkg-a,pkg-b" "main logs package names"
assert_contains "${CAPTURE_STDERR}" "Deleted package pkg-a version 1.0.0 -> 204 No Content" "main deletes old version"
assert_contains "${CAPTURE_STDERR}" "Summary: packages=2 versions=4 kept=3 candidates=1 deleted=1 errors=0" "main summary"
assert_contains "${CAPTURE_STDERR}" "Stage 4 complete" "main final stage log"
}
tests=(
test_trim
test_url_encode
test_resolve_keep_count
test_resolve_keep_count_invalid
test_resolve_package_names
test_resolve_package_names_missing
test_parse_repo_context
test_api_request
test_fetch_package_versions_404
test_fetch_package_versions_paginated
test_collect_package_candidates
test_process_candidates_empty
test_process_candidates
test_main_integration
)
failures=0
for test_name in "${tests[@]}"; do
if run_test "${test_name}" "${test_name}"; then
:
else
failures=$((failures + 1))
fi
done
if (( failures > 0 )); then
printf '%s test(s) failed\n' "${failures}" >&2
exit 1
fi
printf 'all tests passed\n'