mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
Owner review of the v0.19.0 demo + Cisco docs/real spine CLI verification: ACI spines name the VLAN-4 sub-if after the PORT (eth1/49.49, cf. Eth1/29.29-style in show ip ospf neighbors vrf overlay-1); the ISN Nexus names its dot1q-4 sub-if after the VLAN (Ethernet1/1.4). v0.19.0 had the two conventions swapped. 908 tests green.
455 lines
19 KiB
Python
455 lines
19 KiB
Python
"""Regression/coverage tests for PR-3b-batch1 — 21 previously-documented-but-
|
|
unbuilt classes from docs/CONTRACT.md §6, seeded from the existing topology:
|
|
|
|
Access-policy cluster (build/access.py): infraAccPortP, infraHPortS,
|
|
infraPortBlk, infraRsAccBaseGrp, infraAccBndlGrp.
|
|
Route-control cluster (build/l3out.py): rtctrlProfile, rtctrlCtxP,
|
|
rtctrlSubjP, rtctrlMatchRtDest, rtctrlSetComm, rtctrlSetPref, ipRouteP,
|
|
ipNexthopP, l3extMember, ospfExtP.
|
|
EPG/contract classes (build/tenants.py): fvRsPathAtt, vzRsSubjGraphAtt.
|
|
Operational/routing classes (build/routing.py + build/underlay.py):
|
|
uribv4Route, uribv4Nexthop, arpAdjEp, ospfIf.
|
|
|
|
Auth is enforced (PR-4): every client fixture logs in via
|
|
POST /api/aaaLogin.json (admin/cisco) before making authenticated calls,
|
|
following the existing tests/test_pr5_topology_consistency.py pattern.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from aci_sim.build.orchestrator import build_site
|
|
from aci_sim.rest_aci.app import ApicSiteState, make_apic_app
|
|
from aci_sim.topology.loader import load_topology
|
|
|
|
TOPO_PATH = "topology.yaml"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def topo():
|
|
return load_topology(TOPO_PATH)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def site_a(topo):
|
|
return topo.site_by_name("LAB1")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def site_b(topo):
|
|
return topo.site_by_name("LAB2")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def store_a(topo, site_a):
|
|
return build_site(topo, site_a)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def store_b(topo, site_b):
|
|
return build_site(topo, site_b)
|
|
|
|
|
|
def _make_client(topo, site, store) -> TestClient:
|
|
state = ApicSiteState(
|
|
name=site.name, site=site, topo=topo, store=store,
|
|
baseline=copy.deepcopy(store),
|
|
)
|
|
c = TestClient(make_apic_app(state))
|
|
c.post(
|
|
"/api/aaaLogin.json",
|
|
json={"aaaUser": {"attributes": {"name": "admin", "pwd": "cisco"}}},
|
|
)
|
|
return c
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client_a(topo, site_a, store_a):
|
|
return _make_client(topo, site_a, store_a)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client_b(topo, site_b, store_b):
|
|
return _make_client(topo, site_b, store_b)
|
|
|
|
|
|
_ALL_STORES = ["store_a", "store_b"]
|
|
|
|
|
|
def _l1physif_ports(store) -> dict[int, set[str]]:
|
|
"""Return {node_id: {port_id, ...}} from every l1PhysIf DN in the store."""
|
|
ports: dict[int, set[str]] = {}
|
|
for mo in store.by_class("l1PhysIf"):
|
|
m = re.search(r"/node-(\d+)/sys/phys-\[([^\]]+)\]", mo.dn)
|
|
assert m is not None, f"unparsable l1PhysIf dn {mo.dn!r}"
|
|
node_id = int(m.group(1))
|
|
ports.setdefault(node_id, set()).add(m.group(2))
|
|
return ports
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Access-policy cluster
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_access_policy_classes_nonempty(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
for cls in (
|
|
"infraAccPortP", "infraHPortS", "infraPortBlk",
|
|
"infraRsAccBaseGrp", "infraAccBndlGrp",
|
|
):
|
|
mos = store.by_class(cls)
|
|
assert mos, f"{cls} query returned totalCount 0"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_infra_port_blk_ports_exist_in_l1physif_inventory(store_name, request):
|
|
"""Every infraPortBlk from/to port must exist as a real l1PhysIf — the
|
|
port block's fromCard/from/to describe a leaf host-port range, but
|
|
infraPortBlk itself carries no node id, so we resolve it via its parent
|
|
infraHPortS -> infraRsAccBaseGrp -> infraAccBndlGrp path is not node-
|
|
scoped either (access profiles are site-independent in this sim); we
|
|
instead assert the from/to port NUMBERS fall within the host-port range
|
|
(1.._HOST_PORTS) that interfaces.py builds on every leaf, i.e. the block
|
|
is representable on any leaf's real inventory.
|
|
"""
|
|
store = request.getfixturevalue(store_name)
|
|
ports = _l1physif_ports(store)
|
|
all_host_ports = {int(p.split("/")[1]) for ps in ports.values() for p in ps if "/" in p}
|
|
blks = store.by_class("infraPortBlk")
|
|
assert blks, "no infraPortBlk found"
|
|
for blk in blks:
|
|
from_p = int(blk.attrs["fromPort"])
|
|
to_p = int(blk.attrs["toPort"])
|
|
assert blk.attrs["fromCard"] == "1"
|
|
assert from_p <= to_p
|
|
assert from_p in all_host_ports, (
|
|
f"infraPortBlk {blk.dn!r} fromPort {from_p} does not match any "
|
|
f"real l1PhysIf port number"
|
|
)
|
|
assert to_p in all_host_ports, (
|
|
f"infraPortBlk {blk.dn!r} toPort {to_p} does not match any real "
|
|
f"l1PhysIf port number"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_hports_rs_acc_base_grp_targets_real_bndl_grp(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
bndl_dns = {mo.dn for mo in store.by_class("infraAccBndlGrp")}
|
|
assert bndl_dns, "no infraAccBndlGrp found"
|
|
rs_grps = store.by_class("infraRsAccBaseGrp")
|
|
assert rs_grps, "no infraRsAccBaseGrp found"
|
|
for rs in rs_grps:
|
|
assert rs.attrs["tDn"] in bndl_dns, (
|
|
f"infraRsAccBaseGrp {rs.dn!r} tDn {rs.attrs['tDn']!r} does not "
|
|
f"resolve to a real infraAccBndlGrp"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_at_least_one_vpc_bndl_grp(store_name, request):
|
|
"""One infraAccBndlGrp (lagT=node) per border-leaf vPC domain, consistent
|
|
with fabric.py's vpcDom/vpcIf for the same pairs."""
|
|
store = request.getfixturevalue(store_name)
|
|
vpc_bndls = [m for m in store.by_class("infraAccBndlGrp") if m.attrs.get("lagT") == "node"]
|
|
assert vpc_bndls, "expected at least one vPC (lagT=node) infraAccBndlGrp"
|
|
vpc_doms = store.by_class("vpcDom")
|
|
assert vpc_doms, "no vpcDom found to cross-check against"
|
|
|
|
|
|
def test_infra_subtree_full_returns_nested_access_policy_tree(client_a):
|
|
"""rsp-subtree=full on uni/infra returns the nested access-policy tree."""
|
|
resp = client_a.get("/api/mo/uni/infra.json?rsp-subtree=full")
|
|
assert resp.status_code == 200, f"uni/infra -> {resp.status_code}: {resp.text[:200]}"
|
|
data = resp.json()
|
|
assert data["imdata"], "expected uni/infra to resolve to a real MO"
|
|
body = str(data)
|
|
for cls in ("infraAccPortP", "infraHPortS", "infraPortBlk", "infraRsAccBaseGrp", "infraAccBndlGrp"):
|
|
assert cls in body, f"{cls} missing from uni/infra rsp-subtree=full response"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Route-control cluster
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_route_control_classes_nonempty(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
for cls in (
|
|
"rtctrlProfile", "rtctrlCtxP", "rtctrlSubjP", "rtctrlMatchRtDest",
|
|
"rtctrlSetComm", "rtctrlSetPref", "ipRouteP", "ipNexthopP",
|
|
"l3extMember", "ospfExtP",
|
|
):
|
|
mos = store.by_class(cls)
|
|
assert mos, f"{cls} query returned totalCount 0"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_rtctrl_match_rt_dest_uses_real_bd_subnet(store_name, request, topo):
|
|
store = request.getfixturevalue(store_name)
|
|
all_subnets = {
|
|
cidr
|
|
for tenant in topo.tenants
|
|
for bd in tenant.bds
|
|
for cidr in bd.subnets
|
|
}
|
|
dests = store.by_class("rtctrlMatchRtDest")
|
|
assert dests, "no rtctrlMatchRtDest found"
|
|
for d in dests:
|
|
assert d.attrs["ip"] in all_subnets, (
|
|
f"rtctrlMatchRtDest {d.dn!r} ip {d.attrs['ip']!r} is not a real BD subnet"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_ip_route_p_under_real_rsnode_l3out_att(store_name, request):
|
|
"""Every ipRouteP dn must nest under a real l3extRsNodeL3OutAtt (i.e. an
|
|
actual border-leaf node attachment), per the placement table."""
|
|
store = request.getfixturevalue(store_name)
|
|
rsnode_dns = {mo.dn for mo in store.by_class("l3extRsNodeL3OutAtt")}
|
|
assert rsnode_dns, "no l3extRsNodeL3OutAtt found"
|
|
routes = store.by_class("ipRouteP")
|
|
assert routes, "no ipRouteP found"
|
|
for r in routes:
|
|
parent = r.dn.rsplit("/rt-[", 1)[0]
|
|
assert parent in rsnode_dns, f"ipRouteP {r.dn!r} not nested under a real rsnodeL3OutAtt"
|
|
assert r.attrs["ip"] == "0.0.0.0/0"
|
|
assert r.attrs.get("pref")
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_ip_nexthop_p_matches_csw_peer_ip(store_name, request, topo):
|
|
store = request.getfixturevalue(store_name)
|
|
nhs = store.by_class("ipNexthopP")
|
|
assert nhs, "no ipNexthopP found"
|
|
all_csw_peer_ips = {
|
|
bl.csw.peer_ip
|
|
for site in topo.sites
|
|
for bl in site.border_leaves
|
|
}
|
|
for nh in nhs:
|
|
assert nh.attrs["nhAddr"] in all_csw_peer_ips, (
|
|
f"ipNexthopP {nh.dn!r} nhAddr {nh.attrs['nhAddr']!r} is not a real CSW peer IP"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_l3ext_member_on_real_path(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
path_dns = {mo.dn for mo in store.by_class("l3extRsPathL3OutAtt")}
|
|
assert path_dns, "no l3extRsPathL3OutAtt found"
|
|
members = store.by_class("l3extMember")
|
|
assert members, "no l3extMember found"
|
|
for m in members:
|
|
parent = m.dn.rsplit("/mem-", 1)[0]
|
|
assert parent in path_dns, f"l3extMember {m.dn!r} not nested under a real l3extRsPathL3OutAtt"
|
|
assert m.attrs["side"] == "A"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_ospf_ext_p_coexists_with_bgp_ext_p_on_same_l3out(store_name, request):
|
|
"""Batch-1 design choice: ospfExtP added to the SAME L3Out as the
|
|
existing bgpExtP (real ACI allows OSPF+BGP coexistence on one L3Out)."""
|
|
store = request.getfixturevalue(store_name)
|
|
ospf_ext = store.by_class("ospfExtP")
|
|
bgp_ext = store.by_class("bgpExtP")
|
|
assert ospf_ext, "no ospfExtP found"
|
|
assert bgp_ext, "no bgpExtP found"
|
|
ospf_parents = {mo.dn.rsplit("/ospfExtP", 1)[0] for mo in ospf_ext}
|
|
bgp_parents = {mo.dn.rsplit("/bgpExtP", 1)[0] for mo in bgp_ext}
|
|
assert ospf_parents & bgp_parents, "ospfExtP and bgpExtP do not share a common L3Out parent"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EPG/contract classes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_epg_contract_classes_nonempty(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
for cls in ("fvRsPathAtt", "vzRsSubjGraphAtt"):
|
|
mos = store.by_class(cls)
|
|
assert mos, f"{cls} query returned totalCount 0"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_fv_rs_path_att_tdn_is_real_path_with_existing_port(store_name, request):
|
|
"""Every fvRsPathAtt tDn must be a real path whose port exists as a real
|
|
l1PhysIf (same port an EPG's own endpoints already use)."""
|
|
store = request.getfixturevalue(store_name)
|
|
ports = _l1physif_ports(store)
|
|
bindings = store.by_class("fvRsPathAtt")
|
|
assert bindings, "no fvRsPathAtt found"
|
|
for b in bindings:
|
|
m = re.search(r"paths-(\d+)/pathep-\[([^\]]+)\]", b.attrs["tDn"])
|
|
assert m is not None, f"unparsable fvRsPathAtt tDn {b.attrs['tDn']!r}"
|
|
node_id, port_id = int(m.group(1)), m.group(2)
|
|
assert port_id in ports.get(node_id, set()), (
|
|
f"fvRsPathAtt {b.dn!r} tDn references {port_id!r} on node {node_id}, "
|
|
f"which has no matching l1PhysIf"
|
|
)
|
|
# encap must reuse the EPG's own vlan (parsed from the same store's fvCEp)
|
|
assert b.attrs["encap"].startswith("vlan-")
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_vz_rs_subj_graph_att_on_real_subject(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
subj_dns = {mo.dn for mo in store.by_class("vzSubj")}
|
|
assert subj_dns, "no vzSubj found"
|
|
graphs = store.by_class("vzRsSubjGraphAtt")
|
|
assert graphs, "no vzRsSubjGraphAtt found"
|
|
for g in graphs:
|
|
parent = g.dn.rsplit("/rsSubjGraphAtt", 1)[0]
|
|
assert parent in subj_dns, f"vzRsSubjGraphAtt {g.dn!r} not nested under a real vzSubj"
|
|
assert g.attrs.get("tnVnsAbsGraphName")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Operational/routing classes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_routing_classes_nonempty(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
for cls in ("uribv4Route", "uribv4Nexthop", "arpAdjEp", "ospfIf"):
|
|
mos = store.by_class(cls)
|
|
assert mos, f"{cls} query returned totalCount 0"
|
|
|
|
|
|
_RE_NH_PARENT = re.compile(r"^(.*/rt-\[[^\]]+\])/nh-")
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_uribv4_nexthop_rn_parses_into_4_bracketed_parts(store_name, request):
|
|
"""Every uribv4Nexthop RN must parse into 4 bracketed parts:
|
|
nh-[proto]-[addr]-[ifname]-[vrf] — matching autoACI's
|
|
_RE_NH_PARENT = re.compile(r"^(.*/rt-\\[[^\\]]+\\])/nh-") parent-recovery
|
|
regex (verified read-only against route_table.py / routing_state.py)."""
|
|
store = request.getfixturevalue(store_name)
|
|
nhs = store.by_class("uribv4Nexthop")
|
|
assert nhs, "no uribv4Nexthop found"
|
|
for nh in nhs:
|
|
m = _RE_NH_PARENT.match(nh.dn)
|
|
assert m is not None, f"uribv4Nexthop {nh.dn!r} does not match autoACI's parent-recovery regex"
|
|
rn = nh.dn.rsplit("/nh-", 1)[1]
|
|
bracket_groups = re.findall(r"\[([^\]]*)\]", rn)
|
|
assert len(bracket_groups) == 4, (
|
|
f"uribv4Nexthop {nh.dn!r} RN has {len(bracket_groups)} bracketed "
|
|
f"parts, expected 4 (proto, addr, ifname, vrf)"
|
|
)
|
|
# parent route must actually exist in the store
|
|
parent_dn = m.group(1)
|
|
assert store.get(parent_dn) is not None, f"parent uribv4Route {parent_dn!r} missing"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_uribv4_route_dom_segment_matches_real_vrf(store_name, request, topo):
|
|
store = request.getfixturevalue(store_name)
|
|
routes = store.by_class("uribv4Route")
|
|
assert routes, "no uribv4Route found"
|
|
known_vrf_doms = {
|
|
f"{tenant.name}:{vrf.name}"
|
|
for tenant in topo.tenants
|
|
for vrf in tenant.vrfs
|
|
}
|
|
for r in routes:
|
|
m = re.search(r"/dom-([^/]+)/db-rt/", r.dn)
|
|
assert m is not None, f"unparsable uribv4Route dn {r.dn!r} (no dom- segment)"
|
|
assert m.group(1) in known_vrf_doms, (
|
|
f"uribv4Route {r.dn!r} dom {m.group(1)!r} is not a real tenant:vrf"
|
|
)
|
|
assert r.attrs.get("prefix")
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_arp_adj_ep_count_equals_local_endpoint_count(store_name, request):
|
|
"""arpAdjEp count == (locally-learned) endpoint count."""
|
|
store = request.getfixturevalue(store_name)
|
|
local_ceps = [c for c in store.by_class("fvCEp") if c.attrs.get("lcC") == "learned"]
|
|
arp_adjs = store.by_class("arpAdjEp")
|
|
assert local_ceps, "no locally-learned fvCEp found"
|
|
assert len(arp_adjs) == len(local_ceps), (
|
|
f"arpAdjEp count {len(arp_adjs)} != local endpoint count {len(local_ceps)}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_arp_adj_ep_mac_matches_a_real_endpoint(store_name, request):
|
|
store = request.getfixturevalue(store_name)
|
|
local_ceps = {c.attrs["mac"] for c in store.by_class("fvCEp") if c.attrs.get("lcC") == "learned"}
|
|
for adj in store.by_class("arpAdjEp"):
|
|
assert adj.attrs["mac"] in local_ceps, f"arpAdjEp {adj.dn!r} mac not a real endpoint MAC"
|
|
assert adj.attrs["operSt"] == "up"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_ospf_adj_ep_sits_on_an_ospf_if(store_name, request):
|
|
"""Every ospfAdjEp must nest directly under a real ospfIf (the ospfIf's
|
|
own DN being the ospfAdjEp's parent — CONTRACT.md batch-1 placement:
|
|
ospfIf nested between ospfDom and ospfAdjEp)."""
|
|
store = request.getfixturevalue(store_name)
|
|
ospf_if_dns = {mo.dn for mo in store.by_class("ospfIf")}
|
|
adjs = store.by_class("ospfAdjEp")
|
|
assert adjs, "no ospfAdjEp found (multi-site topology expected)"
|
|
assert ospf_if_dns, "no ospfIf found"
|
|
for adj in adjs:
|
|
parent = adj.dn.rsplit("/adj-[", 1)[0]
|
|
assert parent in ospf_if_dns, f"ospfAdjEp {adj.dn!r} does not sit on a real ospfIf"
|
|
|
|
|
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
|
def test_ospf_if_references_real_port(store_name, request):
|
|
"""ospfIf is now the routed VLAN-4 sub-interface of the spine's ISN
|
|
uplink port, named after the port per real ACI convention (e.g.
|
|
"eth1/49.49" — cf. Eth1/29.29-style names in real spine CLI) — real
|
|
LLDP/physical port state stays on the base port, so this resolves the
|
|
underlying l1PhysIf via the base port (ifId.split('.')[0]), same
|
|
fallback autoACI's topology code uses."""
|
|
store = request.getfixturevalue(store_name)
|
|
ports = _l1physif_ports(store)
|
|
ifs = store.by_class("ospfIf")
|
|
assert ifs, "no ospfIf found"
|
|
for ospf_if in ifs:
|
|
m = re.search(r"/node-(\d+)/sys/ospf/.*?/if-\[([^\]]+)\]", ospf_if.dn)
|
|
assert m is not None, f"unparsable ospfIf dn {ospf_if.dn!r}"
|
|
node_id, port_id = int(m.group(1)), m.group(2)
|
|
base_port, _, sub_num = port_id.partition(".")
|
|
assert sub_num == base_port.split("/")[-1], (
|
|
f"ospfIf {ospf_if.dn!r} id {port_id!r} is not a port-named sub-if"
|
|
)
|
|
assert base_port in ports.get(node_id, set()), (
|
|
f"ospfIf {ospf_if.dn!r} references base port {base_port!r} on node "
|
|
f"{node_id}, which has no matching l1PhysIf"
|
|
)
|
|
assert ospf_if.attrs.get("id") == port_id
|
|
addr = ospf_if.attrs.get("addr", "")
|
|
assert re.fullmatch(r"172\.16\.\d+\.\d+/24", addr), (
|
|
f"ospfIf {ospf_if.dn!r} addr {addr!r} is not a 172.16.{{site}}.0/24 address"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cross-cluster sanity: full-site build still round-trips through the REST
|
|
# layer for at least one class per cluster (auth-enforced client).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[
|
|
"infraAccPortP", "rtctrlProfile", "ipRouteP", "fvRsPathAtt",
|
|
"uribv4Route", "arpAdjEp", "ospfIf", "l3extMember", "ospfExtP",
|
|
"vzRsSubjGraphAtt",
|
|
],
|
|
)
|
|
def test_class_query_returns_total_count_gt_0(client_a, cls):
|
|
resp = client_a.get(f"/api/class/{cls}.json")
|
|
assert resp.status_code == 200, f"{cls} -> {resp.status_code}: {resp.text[:200]}"
|
|
data = resp.json()
|
|
assert int(data["totalCount"]) > 0, f"{cls} totalCount is 0"
|