fix(seo-data): fail-open CLI contract (corrupt store + bad usage always emit JSON)

This commit is contained in:
Bastien Chanot
2026-07-10 01:42:23 +02:00
parent c3a504fbbf
commit 39e227f1c8
3 changed files with 55 additions and 27 deletions
+5 -1
View File
@@ -133,6 +133,7 @@ def inspect(store_path, account, property, url):
"last_crawl": isr.get("lastCrawlTime")} "last_crawl": isr.get("lastCrawlTime")}
def _cli(): def _cli():
try:
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True) sub = p.add_subparsers(dest="cmd", required=True)
pc = sub.add_parser("crux") pc = sub.add_parser("crux")
@@ -151,7 +152,6 @@ def _cli():
pi.add_argument("--property", required=True) pi.add_argument("--property", required=True)
pi.add_argument("--url", required=True) pi.add_argument("--url", required=True)
args = p.parse_args() args = p.parse_args()
try:
if args.cmd == "crux": if args.cmd == "crux":
print(json.dumps(crux(args.url, args.strategy), indent=2)) print(json.dumps(crux(args.url, args.strategy), indent=2))
elif args.cmd == "queries": elif args.cmd == "queries":
@@ -160,6 +160,10 @@ def _cli():
elif args.cmd == "inspect": elif args.cmd == "inspect":
print(json.dumps(inspect(args.store, args.account, args.property, print(json.dumps(inspect(args.store, args.account, args.property,
args.url), indent=2)) args.url), indent=2))
except SystemExit as e: # argparse usage error
if e.code not in (0, None):
print(json.dumps({"status": "error", "reason": "bad_usage"}))
raise # preserve argparse's exit code
except Exception: except Exception:
# Fail-open data contract: ANY unexpected error (HTTP 403/5xx, DNS, # Fail-open data contract: ANY unexpected error (HTTP 403/5xx, DNS,
# timeout) degrades with exit 0 — never a traceback, never empty stdout. # timeout) degrades with exit 0 — never a traceback, never empty stdout.
+18 -1
View File
@@ -77,7 +77,24 @@ SEO_DATA_ENV_FILE=$NOENV bash "$FETCH" bogus-subcmd >/dev/null 2>&1; RC=$?
DG="$(SEO_DATA_ENV_FILE=$NOENV env -u SEO_DATA_MOCK_DIR -u CRUX_API_KEY bash "$FETCH" crux --url https://ex.com)"; RC=$? DG="$(SEO_DATA_ENV_FILE=$NOENV env -u SEO_DATA_MOCK_DIR -u CRUX_API_KEY bash "$FETCH" crux --url https://ex.com)"; RC=$?
has "degrade json" "$DG" '"status": "degraded"' has "degrade json" "$DG" '"status": "degraded"'
[ "$RC" = "0" ] && ok "degrade exit 0" || no "degrade exit 0" "got $RC" [ "$RC" = "0" ] && ok "degrade exit 0" || no "degrade exit 0" "got $RC"
hasnt "no secret echoed" "$DG" 'RT_' # redaction through the real fetch.sh dispatch layer
TMP4="$(mktemp -d)"; RSTORE="$TMP4/rt.json"
python3 "$SD/tokenstore.py" set --file "$RSTORE" --label leaky --refresh-token RT_SECRET_XYZ \
--scopes https://www.googleapis.com/auth/webmasters.readonly --properties sc-domain:z.com >/dev/null
ACCJSON="$(SEO_DATA_ENV_FILE=$NOENV SEO_DATA_STORE="$RSTORE" bash "$FETCH" accounts)"
has "accounts lists label" "$ACCJSON" 'leaky'
hasnt "accounts hides token" "$ACCJSON" 'RT_SECRET_XYZ'
rm -rf "$TMP4"
# corrupted store must degrade with JSON + exit 0 (Fix 1)
TMP5="$(mktemp -d)"; CSTORE="$TMP5/corrupt.json"; printf 'not json {{' > "$CSTORE"
CJ="$(SEO_DATA_ENV_FILE=$NOENV SEO_DATA_STORE="$CSTORE" bash "$FETCH" accounts)"; CRC=$?
has "corrupt store degrades" "$CJ" '"status"'
[ "$CRC" = "0" ] && ok "corrupt store exit 0" || no "corrupt store exit 0" "got $CRC"
rm -rf "$TMP5"
# bad usage (known subcmd, missing flag) must still emit JSON + exit 2 (Fix 2)
BU="$(SEO_DATA_ENV_FILE=$NOENV bash "$FETCH" crux)"; BURC=$?
has "bad usage emits json" "$BU" '"status"'
[ "$BURC" = "2" ] && ok "bad usage exit 2" || no "bad usage exit 2" "got $BURC"
echo "" echo ""
echo "seo-data engine: $PASS pass, $FAIL fail" echo "seo-data engine: $PASS pass, $FAIL fail"
+7
View File
@@ -58,6 +58,7 @@ def _cli():
ps.add_argument(flag, required=True) ps.add_argument(flag, required=True)
ps.add_argument("--scopes", default="") ps.add_argument("--scopes", default="")
ps.add_argument("--properties", default="") ps.add_argument("--properties", default="")
try:
args = p.parse_args() args = p.parse_args()
if args.cmd == "list": if args.cmd == "list":
print(json.dumps({"status": "ok", "accounts": list_accounts(args.file)})) print(json.dumps({"status": "ok", "accounts": list_accounts(args.file)}))
@@ -66,6 +67,12 @@ def _cli():
[s for s in args.scopes.split(",") if s], [s for s in args.scopes.split(",") if s],
[x for x in args.properties.split(",") if x]) [x for x in args.properties.split(",") if x])
print(json.dumps({"status": "ok"})) print(json.dumps({"status": "ok"}))
except SystemExit as e: # argparse usage error
if e.code not in (0, None):
print(json.dumps({"status": "error", "reason": "bad_usage"}))
raise # preserve argparse's exit code
except Exception: # e.g. corrupted store JSON
print(json.dumps({"status": "degraded", "reason": "unexpected_error"}))
if __name__ == "__main__": if __name__ == "__main__":
_cli() _cli()