#!/usr/bin/env bash
# check-redirects.sh: test a redirect map against a live site.
# Usage: ./check-redirects.sh redirect-map.csv
# CSV columns (header row required; old_url, new_url and status must not contain commas, notes can):
#   old_url,new_url,status,...   status = 301 | 308 | 410 | 404
# For 301/308 rows it checks three things:
#   1. the first response is the status you planned (a 308 passes a 301 row, and the reverse)
#   2. the redirect ends on new_url with a 200
#   3. it takes one hop (Google follows up to 10 hops and advises ideally three or fewer; one is the aim)
# For 410/404 rows it checks that the old URL answers with that code and does not redirect.
# Needs only bash and curl. Exit code 1 if any row fails.
set -u
file="${1:?usage: $0 redirect-map.csv}"
pass=0; fail=0
while IFS=, read -r old new status _rest; do
  [ "$old" = "old_url" ] && continue
  [ -z "$old" ] && continue
  status="${status//$'\r'/}"; new="${new//$'\r'/}"
  first=$(curl -s -o /dev/null --max-time 20 --proto '=http,https' -w '%{http_code}' "$old")
  problems=""
  case "$status" in
    301|308)
      read -r last hops final < <(curl -s -o /dev/null -L --max-redirs 10 --max-time 30 --proto '=http,https' --proto-redir '=http,https' \
        -w '%{http_code} %{num_redirects} %{url_effective}\n' "$old")
      case "$first" in 301|308) ;; *) problems="$problems first-response=$first(want $status);";; esac
      [ "$last" = "200" ] || problems="$problems final-status=$last;"
      [ "$final" = "$new" ] || problems="$problems lands-on=$final(want $new);"
      [ "${hops:-0}" -le 1 ] || problems="$problems hops=$hops;"
      ;;
    410|404)
      [ "$first" = "$status" ] || problems="$problems status=$first(want $status);"
      ;;
    *) problems="unknown status column: $status;" ;;
  esac
  if [ -z "$problems" ]; then pass=$((pass+1)); echo "PASS $old"; else fail=$((fail+1)); echo "FAIL $old $problems"; fi
done < "$file"
echo "---"; echo "passed $pass, failed $fail"
[ "$fail" -eq 0 ]
