fix(seo-data): fail-open CLI contract (corrupt store + bad usage always emit JSON)
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user