#!/usr/bin/env bash # # End-to-end OAuth2 flow against tests/mock_oidc_server.py. Exercises # forge_login.sh, forge_auth.py, the credential helper, and the # username-mismatch guard in a sandboxed $HOME. Never touches a real # Gitea. set -euo pipefail here="$(cd "$(dirname "$0")" && pwd -P)" root="$here/.." # Disable every interactive credential fallback so git fails loudly # rather than prompting (VSCode, X11/Wayland askpass, terminal). export GIT_TERMINAL_PROMPT=0 export GCM_INTERACTIVE=Never unset GIT_ASKPASS SSH_ASKPASS \ VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE \ VSCODE_GIT_ASKPASS_EXTRA_ARGS VSCODE_GIT_IPC_HANDLE \ DISPLAY WAYLAND_DISPLAY tmp="$(mktemp -d)" cleanup() { [ -n "${mock_pid:-}" ] && kill "$mock_pid" 2>/dev/null || true [ -n "${browser_pid:-}" ] && kill "$browser_pid" 2>/dev/null || true [ -n "${login_pid:-}" ] && kill "$login_pid" 2>/dev/null || true rm -rf "$tmp" } trap cleanup EXIT INT TERM export HOME="$tmp/home" mkdir -p "$HOME/.local/bin" "$HOME/.config" export GIT_CONFIG_GLOBAL="$HOME/.gitconfig" export GIT_CONFIG_SYSTEM=/dev/null touch "$GIT_CONFIG_GLOBAL" export FSDGG_AUTH_STORE_PATH="$HOME/.forge-stack-devpi-gateway-gitea/client-auth.json" pass=0 fail=0 assert() { local msg="$1"; shift if "$@"; then printf '[ok] %s\n' "$msg" pass=$((pass + 1)) else printf '[FAIL] %s\n' "$msg" fail=$((fail + 1)) fi } # --- Start the mock Gitea ------------------------------------------ MOCK_OIDC_USERNAME=integration-user \ python3 -u "$here/mock_oidc_server.py" >"$tmp/mock.url" 2>"$tmp/mock.log" & mock_pid=$! # Wait for the server to announce its URL. for _ in $(seq 1 40); do mock_url="$(head -1 "$tmp/mock.url" 2>/dev/null || true)" [ -n "$mock_url" ] && break sleep 0.1 done [ -n "$mock_url" ] || { cat "$tmp/mock.log"; echo 'mock server never came up' >&2; exit 1; } printf '[info] mock Gitea: %s\n' "$mock_url" # --- Pick an unused loopback port for the callback ----------------- loopback_port="$(python3 -c ' import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) print(s.getsockname()[1]) ')" redirect_uri="http://127.0.0.1:${loopback_port}/callback" # --- Synthesize .env inside the welcome repo root ------------------ env_file="$tmp/env" cat >"$env_file" </dev/null | grep -q ":${loopback_port} " \ || netstat -tln 2>/dev/null | grep -q ":${loopback_port} "; then break fi sleep 0.1 done url="$(cat "$tmp/authorize.url" 2>/dev/null || true)" if [ -n "$url" ]; then curl -fsSL --max-time 10 "$url" >/dev/null 2>"$tmp/browser.log" || true fi ) & browser_pid=$! python3 "$root/scripts/forge_auth.py" login --no-browser 2> >(tee "$tmp/login.log" >&2) \ < <(printf '') & login_pid=$! for _ in $(seq 1 80); do if grep -qE 'https?://.*authorize\?' "$tmp/login.log" 2>/dev/null; then grep -oE 'https?://[^ ]+authorize\?[^ ]+' "$tmp/login.log" | head -1 >"$tmp/authorize.url" break fi sleep 0.1 done wait "$login_pid" || { echo 'forge_auth login failed'; cat "$tmp/login.log"; exit 1; } wait "$browser_pid" 2>/dev/null || true assert 'client-auth.json was written' test -f "$FSDGG_AUTH_STORE_PATH" assert 'client-auth.json is mode 0600' \ bash -c '[ "$(stat -c %a "$FSDGG_AUTH_STORE_PATH" 2>/dev/null || stat -f %A "$FSDGG_AUTH_STORE_PATH")" = "600" ]' user="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["username"])' "$FSDGG_AUTH_STORE_PATH")" assert 'username captured from userinfo' test "$user" = 'integration-user' token="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["gitea_access_token"])' "$FSDGG_AUTH_STORE_PATH")" assert 'gitea_access_token captured' bash -c "[ -n \"$token\" ] && [[ '$token' == access-* ]]" refresh="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["_forge_refresh_token"])' "$FSDGG_AUTH_STORE_PATH")" assert 'refresh token captured' bash -c "[ -n \"$refresh\" ] && [[ '$refresh' == refresh-* ]]" # --- Install the git credential helper ----------------------------- bash "$root/scripts/install-git-credential-helper.sh" >"$tmp/inst.log" 2>&1 \ || { cat "$tmp/inst.log"; echo 'helper install failed' >&2; exit 1; } mock_host="$(python3 -c 'import sys,urllib.parse as u; p=u.urlsplit(sys.argv[1]); print(f"{p.hostname}:{p.port}")' "$mock_url")" # --- Drive `git credential fill` ----------------------------------- credfill_out="$( printf 'protocol=http\nhost=%s\npath=someorg/somerepo.git\n\n' "$mock_host" \ | PATH="$HOME/.local/bin:$PATH" git credential fill 2>"$tmp/credfill.err" )" || { echo 'git credential fill failed'; cat "$tmp/credfill.err"; exit 1; } got_pass="$(printf '%s\n' "$credfill_out" | awk -F= '/^password=/ {print $2}')" got_user="$(printf '%s\n' "$credfill_out" | awk -F= '/^username=/ {print $2}')" assert 'git credential fill returns username from OAuth' test "$got_user" = 'integration-user' assert 'git credential fill returns the OAuth access token as password' test "$got_pass" = "$token" # --- Unrelated-host probe: must NOT leak credentials --------------- other_out="$( printf 'protocol=https\nhost=github.com\npath=x/y.git\n\n' \ | "$HOME/.local/bin/git-credential-forge" get )" assert 'unrelated host gets no username' bash -c "! printf '%s\n' \"$other_out\" | grep -q '^username='" assert 'unrelated host gets no password' bash -c "! printf '%s\n' \"$other_out\" | grep -q '^password='" # --- Refresh path: force a refresh and re-run credential fill ------ python3 "$root/scripts/forge_auth.py" refresh --force >"$tmp/refresh.log" 2>&1 \ || { cat "$tmp/refresh.log"; echo 'refresh failed' >&2; exit 1; } new_token="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["gitea_access_token"])' "$FSDGG_AUTH_STORE_PATH")" assert 'refresh rotates the access token' bash -c "[ \"$new_token\" != \"$token\" ]" credfill_out2="$( printf 'protocol=http\nhost=%s\n\n' "$mock_host" \ | PATH="$HOME/.local/bin:$PATH" git credential fill 2>>"$tmp/credfill.err" )" got_pass2="$(printf '%s\n' "$credfill_out2" | awk -F= '/^password=/ {print $2}')" assert 'git credential fill picks up the rotated token' test "$got_pass2" = "$new_token" # --- Logout removes the file --------------------------------------- python3 "$root/scripts/forge_auth.py" logout >"$tmp/logout.log" 2>&1 assert 'logout removes client-auth.json' bash -c "[ ! -f \"$FSDGG_AUTH_STORE_PATH\" ]" # --- Username-mismatch guard --------------------------------------- loopback_port2="$(python3 -c ' import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) print(s.getsockname()[1]) ')" export FSDGG_CLI_REDIRECT_URI="http://127.0.0.1:${loopback_port2}/callback" export FORGE_GITEA_USERNAME="not-the-right-user" rm -f "$tmp/authorize.url" ( for _ in $(seq 1 50); do if ss -tln 2>/dev/null | grep -q ":${loopback_port2} " \ || netstat -tln 2>/dev/null | grep -q ":${loopback_port2} "; then break fi sleep 0.1 done url="$(cat "$tmp/authorize.url" 2>/dev/null || true)" if [ -n "$url" ]; then curl -fsSL --max-time 10 "$url" >/dev/null 2>"$tmp/browser2.log" || true fi ) & browser_pid=$! set +e python3 "$root/scripts/forge_auth.py" login --no-browser \ 2> >(tee "$tmp/mismatch.log" >&2) & mismatch_pid=$! for _ in $(seq 1 80); do if grep -qE 'https?://.*authorize\?' "$tmp/mismatch.log" 2>/dev/null; then grep -oE 'https?://[^ ]+authorize\?[^ ]+' "$tmp/mismatch.log" | head -1 >"$tmp/authorize.url" break fi sleep 0.1 done wait "$mismatch_pid" mismatch_rc=$? wait "$browser_pid" 2>/dev/null || true set -e assert 'login fails when username mismatches FORGE_GITEA_USERNAME' bash -c "[ $mismatch_rc -ne 0 ]" assert 'mismatch error references the actual Gitea username' \ grep -q 'integration-user' "$tmp/mismatch.log" assert 'mismatch error references the expected username' \ grep -q 'not-the-right-user' "$tmp/mismatch.log" assert 'mismatch error points at Gitea logout URL' \ grep -qE '/user/logout\?redirect_to=%2Fuser%2Flogin' "$tmp/mismatch.log" assert 'mismatch does NOT create client-auth.json' \ bash -c "[ ! -f \"$FSDGG_AUTH_STORE_PATH\" ]" auth_url="$(cat "$tmp/authorize.url" 2>/dev/null || true)" assert 'authorize URL includes prompt=login' \ bash -c "printf '%s\n' \"$auth_url\" | grep -q 'prompt=login'" assert 'authorize URL carries login_hint' \ bash -c "printf '%s\n' \"$auth_url\" | grep -q 'login_hint=not-the-right-user'" printf '\n%d pass / %d fail\n' "$pass" "$fail" [ "$fail" -eq 0 ]