Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan GET collection endpoints for empty list payloads across all series."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.openstack.contract_loader import list_series, load_series_pack # noqa: E402
|
||||
from app.openstack.surface_probe import ( # noqa: E402
|
||||
activate_series,
|
||||
fill_path,
|
||||
http_request,
|
||||
issue_token,
|
||||
)
|
||||
|
||||
|
||||
def _is_empty_list_payload(body: object) -> tuple[bool, str | None]:
|
||||
if not isinstance(body, dict):
|
||||
return False, None
|
||||
if body.get("data") == []:
|
||||
return True, "data"
|
||||
for key, value in body.items():
|
||||
if key in {"links", "metadata", "versions", "version", "id", "status"}:
|
||||
continue
|
||||
if isinstance(value, list) and len(value) == 0:
|
||||
return True, key
|
||||
return False, None
|
||||
|
||||
|
||||
def _is_top_level_collection(op) -> bool: # noqa: ANN001
|
||||
if op.method != "GET":
|
||||
return False
|
||||
if op.kind in {"collection", "detail"}:
|
||||
return "{" not in op.path or op.path.rstrip("/").endswith("/detail")
|
||||
if op.kind == "custom" and "{" not in op.path:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", default="http://api-gateway:5000")
|
||||
parser.add_argument("--series", action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
host = args.host.rstrip("/")
|
||||
if args.series:
|
||||
series_list = args.series
|
||||
else:
|
||||
series_list = [str(item["series"]) for item in list_series()]
|
||||
|
||||
token, _ = issue_token(host, user="admin", project="admin")
|
||||
empties: list[tuple[str, str, str, str, str | None, int, str | None]] = []
|
||||
checked = 0
|
||||
|
||||
for series in series_list:
|
||||
activate_series(host, series)
|
||||
packs = load_series_pack(series)
|
||||
for name, pack in sorted(packs.items()):
|
||||
for op in pack.operations:
|
||||
if not _is_top_level_collection(op):
|
||||
continue
|
||||
path = fill_path(op.path)
|
||||
status, body = http_request("GET", f"{host}{path}", token=token, service=name)
|
||||
checked += 1
|
||||
empty, key = _is_empty_list_payload(body)
|
||||
if empty:
|
||||
empties.append(
|
||||
(
|
||||
series,
|
||||
name,
|
||||
op.path,
|
||||
op.resource_type,
|
||||
op.collection_key,
|
||||
status,
|
||||
key,
|
||||
)
|
||||
)
|
||||
|
||||
by_path: dict[tuple[str, str, str, str | None], list[str]] = defaultdict(list)
|
||||
for series, svc, path, rtype, ckey, _status, _key in empties:
|
||||
by_path[(svc, rtype, path, ckey)].append(series)
|
||||
|
||||
print(json.dumps({"checked": checked, "empty": len(empties), "unique": len(by_path)}, indent=2))
|
||||
print("\n=== EMPTY COLLECTIONS ===")
|
||||
for (svc, rtype, path, ckey), serieses in sorted(by_path.items()):
|
||||
print(
|
||||
f"{svc:12} {rtype:28} key={str(ckey):24} {path} "
|
||||
f"series={','.join(sorted(set(serieses)))}"
|
||||
)
|
||||
return 0 if not by_path else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user