forgejo-mcp/test/e2e/attachments.sh
Christoph Görn 30c13037ef
test: ✅ add e2e attachment test script
Verifies v2.19.0-alpha.1 attachment tools end-to-end against live Forgejo:
create issue, upload file, list/get metadata, sha256-verify both the
browser_download_url (curl+token) and the MCP-inline base64 blob paths.
Cleans up (delete attachment, close issue) on success or failure via trap.
2026-04-27 09:06:19 +02:00

156 lines
7 KiB
Bash
Executable file

#!/usr/bin/env bash
# E2E test for issue-attachment MCP tools (v2.19.0-alpha.1+).
#
# Validates that ./forgejo-mcp --cli can:
# 1. create an issue
# 2. upload an attachment
# 3. list attachments and see the one we just uploaded
# 4. fetch single-attachment metadata, with browser_download_url present
# 5. fetch the URL with the configured token and recover the original bytes
#
# Cleans up (deletes attachment, closes issue) on success or failure.
#
# Usage:
# FORGEJO_URL=https://codeberg.org \
# FORGEJO_ACCESS_TOKEN=... \
# OWNER=goern REPO=forgejo-mcp \
# ./test/e2e/attachments.sh
#
# Requires: jq, curl, base64, ./forgejo-mcp built (run `make build` first).
set -euo pipefail
: "${FORGEJO_URL:?FORGEJO_URL required (e.g. https://codeberg.org)}"
: "${FORGEJO_ACCESS_TOKEN:?FORGEJO_ACCESS_TOKEN required}"
OWNER="${OWNER:-goern}"
REPO="${REPO:-forgejo-mcp}"
BIN="${BIN:-./forgejo-mcp}"
[[ -x "$BIN" ]] || { echo "binary not found at $BIN — run 'make build' first" >&2; exit 1; }
command -v jq >/dev/null || { echo "jq required" >&2; exit 1; }
command -v curl >/dev/null || { echo "curl required" >&2; exit 1; }
# --- helpers ----------------------------------------------------------------
# Run a forgejo-mcp tool and unwrap the {"Result":...} envelope.
# Tools wrap output as [{"type":"text","text":"{\"Result\":...}"}].
mcp() {
local tool="$1" args="$2"
"$BIN" --cli "$tool" --args "$args" 2>/dev/null \
| jq -r '.[] | select(.type=="text") | .text' \
| jq '.Result // .'
}
# Status line on a green background; failure on red.
ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
fail() { printf '\033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
step() { printf '\n\033[1m▶ %s\033[0m\n' "$*"; }
# --- state for cleanup ------------------------------------------------------
ISSUE_INDEX=""
ATTACH_ID=""
cleanup() {
local rc=$?
set +e
if [[ -n "$ATTACH_ID" && -n "$ISSUE_INDEX" ]]; then
echo "[cleanup] delete attachment $ATTACH_ID"
mcp delete_issue_attachment \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" --argjson a "$ATTACH_ID" \
'{owner:$o, repo:$r, index:$i, attachment_id:$a}')" >/dev/null
fi
if [[ -n "$ISSUE_INDEX" ]]; then
echo "[cleanup] close issue #$ISSUE_INDEX"
mcp issue_state_change \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" \
'{owner:$o, repo:$r, index:$i, state:"closed"}')" >/dev/null
fi
exit "$rc"
}
trap cleanup EXIT
# --- test payload -----------------------------------------------------------
PAYLOAD="forgejo-mcp e2e attachment test $(date -u +%s) $RANDOM"
PAYLOAD_FILE="$(mktemp)"
printf '%s' "$PAYLOAD" > "$PAYLOAD_FILE"
PAYLOAD_B64="$(base64 -w0 "$PAYLOAD_FILE")"
PAYLOAD_SHA="$(printf '%s' "$PAYLOAD" | sha256sum | cut -d' ' -f1)"
# --- 1. create issue --------------------------------------------------------
step "create issue"
ISSUE_JSON="$(mcp create_issue \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" \
'{owner:$o, repo:$r, title:"[e2e] attachment test", body:"transient — will be auto-closed"}')")"
ISSUE_INDEX="$(echo "$ISSUE_JSON" | jq '.number')"
[[ "$ISSUE_INDEX" =~ ^[0-9]+$ ]] || fail "create_issue did not return a numeric .number; got: $ISSUE_JSON"
ok "issue #$ISSUE_INDEX created"
# --- 2. upload attachment ---------------------------------------------------
step "upload attachment"
CREATE_JSON="$(mcp create_issue_attachment \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" \
--arg c "$PAYLOAD_B64" --arg fn "e2e-test.txt" --arg mt "text/plain" \
'{owner:$o, repo:$r, index:$i, content:$c, filename:$fn, mime_type:$mt}')")"
ATTACH_ID="$(echo "$CREATE_JSON" | jq '.id')"
ATTACH_URL="$(echo "$CREATE_JSON" | jq -r '.browser_download_url')"
ATTACH_SIZE="$(echo "$CREATE_JSON" | jq '.size')"
[[ "$ATTACH_ID" =~ ^[0-9]+$ ]] || fail "create_issue_attachment did not return numeric .id; got: $CREATE_JSON"
[[ "$ATTACH_URL" == http*://* ]] || fail "create did not return browser_download_url; got: $ATTACH_URL"
[[ "$ATTACH_SIZE" -eq "${#PAYLOAD}" ]] || fail "uploaded size $ATTACH_SIZE != local size ${#PAYLOAD}"
ok "attachment $ATTACH_ID uploaded ($ATTACH_SIZE bytes)"
ok "browser_download_url = $ATTACH_URL"
# --- 3. list attachments ----------------------------------------------------
step "list attachments"
LIST_JSON="$(mcp list_issue_attachments \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" \
'{owner:$o, repo:$r, index:$i}')")"
LIST_LEN="$(echo "$LIST_JSON" | jq 'length')"
[[ "$LIST_LEN" -ge 1 ]] || fail "list returned $LIST_LEN entries; expected ≥ 1"
LIST_HAS_ID="$(echo "$LIST_JSON" | jq --argjson a "$ATTACH_ID" 'map(.id) | index($a) != null')"
[[ "$LIST_HAS_ID" == "true" ]] || fail "uploaded attachment $ATTACH_ID not in list: $LIST_JSON"
ok "list contains attachment $ATTACH_ID"
# --- 4. get single attachment ----------------------------------------------
step "get attachment metadata"
GET_JSON="$(mcp get_issue_attachment \
"$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" --argjson a "$ATTACH_ID" \
'{owner:$o, repo:$r, index:$i, attachment_id:$a}')")"
GET_URL="$(echo "$GET_JSON" | jq -r '.browser_download_url')"
GET_NAME="$(echo "$GET_JSON" | jq -r '.name')"
[[ "$GET_URL" == "$ATTACH_URL" ]] || fail "get returned different URL: $GET_URL vs $ATTACH_URL"
[[ "$GET_NAME" == "e2e-test.txt" ]] || fail "get returned wrong name: $GET_NAME"
ok "get returns matching url + name"
# --- 5. download via browser_download_url + auth header --------------------
step "fetch browser_download_url with token (over-cap fall-through path)"
FETCHED="$(curl -sS -f -H "Authorization: token $FORGEJO_ACCESS_TOKEN" "$ATTACH_URL")"
FETCHED_SHA="$(printf '%s' "$FETCHED" | sha256sum | cut -d' ' -f1)"
[[ "$FETCHED_SHA" == "$PAYLOAD_SHA" ]] || fail "downloaded sha $FETCHED_SHA != uploaded sha $PAYLOAD_SHA"
ok "download bytes match (sha256=$PAYLOAD_SHA)"
# --- 6. download via MCP tool (under-cap inline path) ----------------------
step "download via MCP tool (under-cap → inline base64 blob)"
DL_RAW="$("$BIN" --cli download_issue_attachment \
--args "$(jq -nc --arg o "$OWNER" --arg r "$REPO" --argjson i "$ISSUE_INDEX" --argjson a "$ATTACH_ID" \
'{owner:$o, repo:$r, index:$i, attachment_id:$a}')" 2>/dev/null)"
DL_TEXT="$(echo "$DL_RAW" | jq -r '.[] | select(.type=="text") | .text')"
DL_INLINE="$(echo "$DL_TEXT" | jq '.inline')"
[[ "$DL_INLINE" == "true" ]] || fail "expected inline=true for ${#PAYLOAD}-byte payload; got: $DL_TEXT"
DL_BLOB="$(echo "$DL_RAW" | jq -r '.[] | select(.type=="resource") | .resource.blob')"
[[ -n "$DL_BLOB" && "$DL_BLOB" != "null" ]] || fail "no resource.blob in download response: $DL_RAW"
DL_BYTES="$(printf '%s' "$DL_BLOB" | base64 -d)"
DL_SHA="$(printf '%s' "$DL_BYTES" | sha256sum | cut -d' ' -f1)"
[[ "$DL_SHA" == "$PAYLOAD_SHA" ]] || fail "MCP-downloaded sha $DL_SHA != uploaded sha $PAYLOAD_SHA"
ok "MCP download bytes match (sha256=$PAYLOAD_SHA)"
echo
ok "E2E PASSED — issue #$ISSUE_INDEX, attachment $ATTACH_ID"