achievement-report-demo.sh 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. REQUEST_DIR="${SCRIPT_DIR}/../src/test/resources/requests"
  5. VALID_REQUEST="${REQUEST_DIR}/exam-sprint-achievement-report-request.json"
  6. INVALID_REQUEST="${REQUEST_DIR}/exam-sprint-achievement-report-invalid-request.json"
  7. BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
  8. OUTPUT_PATH="${OUTPUT_PATH:-${PWD}/achievement-report-demo.pdf}"
  9. HTML_OUTPUT_PATH="${HTML_OUTPUT_PATH:-${PWD}/achievement-report-preview.html}"
  10. MAX_ATTEMPTS="${MAX_ATTEMPTS:-30}"
  11. POLL_INTERVAL_SECONDS="${POLL_INTERVAL_SECONDS:-1}"
  12. JSON_PARSER=""
  13. TMP_FILES=()
  14. usage() {
  15. cat <<EOF
  16. Usage:
  17. $(basename "$0") success
  18. $(basename "$0") invalid
  19. $(basename "$0") --help
  20. Modes:
  21. success Submit the valid sample request, poll report status, then download the PDF and preview HTML.
  22. invalid Submit the invalid sample request and print the validation error response.
  23. Environment variables:
  24. BASE_URL API base URL. Default: http://127.0.0.1:8080
  25. OUTPUT_PATH PDF output path for success mode.
  26. Default: current working directory/achievement-report-demo.pdf
  27. HTML_OUTPUT_PATH HTML preview output path for success mode.
  28. Default: current working directory/achievement-report-preview.html
  29. MAX_ATTEMPTS Polling attempts for success mode. Default: 30
  30. POLL_INTERVAL_SECONDS Seconds between polling attempts. Default: 1
  31. Examples:
  32. $(basename "$0") success
  33. BASE_URL=http://127.0.0.1:8081 $(basename "$0") invalid
  34. EOF
  35. }
  36. cleanup() {
  37. if [ "${#TMP_FILES[@]}" -gt 0 ]; then
  38. rm -f "${TMP_FILES[@]}"
  39. fi
  40. }
  41. trap cleanup EXIT
  42. die() {
  43. printf 'Error: %s\n' "$1" >&2
  44. exit 1
  45. }
  46. make_temp_file() {
  47. local file
  48. file="$(mktemp)"
  49. TMP_FILES+=("$file")
  50. printf '%s\n' "$file"
  51. }
  52. require_command() {
  53. command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
  54. }
  55. select_json_parser() {
  56. if command -v jq >/dev/null 2>&1; then
  57. JSON_PARSER="jq"
  58. elif command -v python3 >/dev/null 2>&1; then
  59. JSON_PARSER="python3"
  60. else
  61. die "JSON parsing requires jq or python3, but neither was found in PATH."
  62. fi
  63. }
  64. json_get() {
  65. local file="$1"
  66. local path="$2"
  67. if [ "$JSON_PARSER" = "jq" ]; then
  68. jq -er ".$path" "$file"
  69. else
  70. python3 - "$file" "$path" <<'PY'
  71. import json
  72. import sys
  73. from pathlib import Path
  74. file_path = Path(sys.argv[1])
  75. path = [part for part in sys.argv[2].split('.') if part]
  76. value = json.loads(file_path.read_text(encoding='utf-8'))
  77. for part in path:
  78. if isinstance(value, dict) and part in value:
  79. value = value[part]
  80. else:
  81. sys.exit(1)
  82. if value is None:
  83. sys.exit(1)
  84. if isinstance(value, (dict, list)):
  85. print(json.dumps(value, ensure_ascii=False))
  86. else:
  87. print(value)
  88. PY
  89. fi
  90. }
  91. pretty_print_json() {
  92. local file="$1"
  93. if [ "$JSON_PARSER" = "jq" ]; then
  94. jq . "$file"
  95. else
  96. python3 - "$file" <<'PY'
  97. import json
  98. import sys
  99. from pathlib import Path
  100. file_path = Path(sys.argv[1])
  101. data = json.loads(file_path.read_text(encoding='utf-8'))
  102. print(json.dumps(data, ensure_ascii=False, indent=2))
  103. PY
  104. fi
  105. }
  106. normalize_url() {
  107. local url="$1"
  108. if [[ "$url" =~ ^https?:// ]]; then
  109. printf '%s\n' "$url"
  110. elif [[ "$url" == /* ]]; then
  111. printf '%s%s\n' "${BASE_URL%/}" "$url"
  112. else
  113. printf '%s/%s\n' "${BASE_URL%/}" "$url"
  114. fi
  115. }
  116. post_json() {
  117. local url="$1"
  118. local payload="$2"
  119. local output="$3"
  120. curl -sS -o "$output" -w '%{http_code}' -X POST -H 'Content-Type: application/json' --data "@$payload" "$url"
  121. }
  122. get_json() {
  123. local url="$1"
  124. local output="$2"
  125. curl -sS -o "$output" -w '%{http_code}' "$url"
  126. }
  127. download_file() {
  128. local url="$1"
  129. local output="$2"
  130. curl -sS -L -o "$output" -w '%{http_code}' "$url"
  131. }
  132. ensure_request_files() {
  133. [ -f "$VALID_REQUEST" ] || die "Missing request fixture: $VALID_REQUEST"
  134. [ -f "$INVALID_REQUEST" ] || die "Missing request fixture: $INVALID_REQUEST"
  135. }
  136. run_success_mode() {
  137. local submit_url submit_body submit_status report_id query_url query_body query_status current_status
  138. local download_url download_target download_status preview_html_url preview_html_target preview_html_status attempt
  139. submit_url="${BASE_URL%/}/api/exam-sprint/reports"
  140. submit_body="$(make_temp_file)"
  141. submit_status="$(post_json "$submit_url" "$VALID_REQUEST" "$submit_body")" || die "Failed to submit request to ${submit_url}"
  142. if [ "$submit_status" != "202" ]; then
  143. printf 'Submit request failed with HTTP %s\n' "$submit_status" >&2
  144. pretty_print_json "$submit_body" >&2
  145. exit 1
  146. fi
  147. report_id="$(json_get "$submit_body" 'data.reportId')" || die "Accepted response did not contain data.reportId"
  148. printf 'Report created: %s\n' "$report_id"
  149. query_url="${BASE_URL%/}/api/exam-sprint/reports/${report_id}"
  150. query_body="$(make_temp_file)"
  151. for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do
  152. query_status="$(get_json "$query_url" "$query_body")" || die "Failed to query report status from ${query_url}"
  153. if [ "$query_status" != "200" ]; then
  154. printf 'Query report failed with HTTP %s\n' "$query_status" >&2
  155. pretty_print_json "$query_body" >&2
  156. exit 1
  157. fi
  158. current_status="$(json_get "$query_body" 'data.generationStatus')" || die "Report response did not contain data.generationStatus"
  159. printf 'Poll %d/%s: %s\n' "$attempt" "$MAX_ATTEMPTS" "$current_status"
  160. case "$current_status" in
  161. SUCCESS)
  162. download_url="$(json_get "$query_body" 'data.downloadUrl')" || die "Successful report did not contain data.downloadUrl"
  163. download_url="$(normalize_url "$download_url")"
  164. download_target="$(make_temp_file)"
  165. download_status="$(download_file "$download_url" "$download_target")" || die "Failed to download PDF from ${download_url}"
  166. if [ "$download_status" != "200" ]; then
  167. printf 'Download failed with HTTP %s\n' "$download_status" >&2
  168. pretty_print_json "$download_target" >&2
  169. exit 1
  170. fi
  171. preview_html_url="$(json_get "$query_body" 'data.previewHtmlUrl')" || die "Successful report did not contain data.previewHtmlUrl"
  172. preview_html_url="$(normalize_url "$preview_html_url")"
  173. preview_html_target="$(make_temp_file)"
  174. preview_html_status="$(download_file "$preview_html_url" "$preview_html_target")" || die "Failed to download preview HTML from ${preview_html_url}"
  175. if [ "$preview_html_status" != "200" ]; then
  176. printf 'Preview HTML download failed with HTTP %s\n' "$preview_html_status" >&2
  177. pretty_print_json "$preview_html_target" >&2
  178. exit 1
  179. fi
  180. mv "$download_target" "$OUTPUT_PATH"
  181. mv "$preview_html_target" "$HTML_OUTPUT_PATH"
  182. printf 'PDF downloaded to: %s\n' "$OUTPUT_PATH"
  183. printf 'Preview HTML downloaded to: %s\n' "$HTML_OUTPUT_PATH"
  184. return 0
  185. ;;
  186. FAILED|EXPIRED)
  187. printf 'Report ended with status %s\n' "$current_status" >&2
  188. pretty_print_json "$query_body" >&2
  189. exit 1
  190. ;;
  191. esac
  192. sleep "$POLL_INTERVAL_SECONDS"
  193. done
  194. printf 'Report did not reach SUCCESS within %s attempts\n' "$MAX_ATTEMPTS" >&2
  195. pretty_print_json "$query_body" >&2
  196. exit 1
  197. }
  198. run_invalid_mode() {
  199. local submit_url response_body response_status
  200. submit_url="${BASE_URL%/}/api/exam-sprint/reports"
  201. response_body="$(make_temp_file)"
  202. response_status="$(post_json "$submit_url" "$INVALID_REQUEST" "$response_body")" || die "Failed to submit invalid request to ${submit_url}"
  203. printf 'HTTP %s\n' "$response_status"
  204. pretty_print_json "$response_body"
  205. if [ "$response_status" != "400" ]; then
  206. die "Expected HTTP 400 for invalid request, got ${response_status}"
  207. fi
  208. }
  209. main() {
  210. local mode="${1:-}"
  211. if [ "$#" -gt 1 ]; then
  212. usage
  213. exit 1
  214. fi
  215. case "$mode" in
  216. ""|--help|-h|help)
  217. usage
  218. ;;
  219. success)
  220. require_command curl
  221. select_json_parser
  222. ensure_request_files
  223. run_success_mode
  224. ;;
  225. invalid)
  226. require_command curl
  227. select_json_parser
  228. ensure_request_files
  229. run_invalid_mode
  230. ;;
  231. *)
  232. usage
  233. exit 1
  234. ;;
  235. esac
  236. }
  237. main "$@"