Initial Commit
This commit is contained in:
237
tests/test_forge_auth_integration.sh
Executable file
237
tests/test_forge_auth_integration.sh
Executable file
@@ -0,0 +1,237 @@
|
||||
#!/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" <<EOF
|
||||
FORGE_GITEA_URL=$mock_url
|
||||
FORGE_GITEA_USERNAME=integration-user
|
||||
FSDGG_CLI_CLIENT_ID=integration-client
|
||||
FSDGG_CLI_REDIRECT_URI=$redirect_uri
|
||||
FORGE_INSECURE_TLS=0
|
||||
EOF
|
||||
|
||||
# Export mock env; scripts pick these up via load_env.
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$env_file"
|
||||
set +a
|
||||
|
||||
# Fake browser: once the loopback listener is up, GET the authorise
|
||||
# URL printed to the login log; the mock 302-redirects to the callback.
|
||||
(
|
||||
# Wait for the loopback listener to come up, then drive it.
|
||||
for _ in $(seq 1 50); do
|
||||
if ss -tln 2>/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 ]
|
||||
Reference in New Issue
Block a user