"""Integration: update_group_members over MCP (#5487) against Postgres. A host joins a group by its linked target, a group rule covering that group is named in the answer, and the change is audited as " via MCP".""" from __future__ import annotations import asyncio import json import os import uuid import pytest pytestmark = pytest.mark.integration @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app return create_app(testing=False) def test_a_host_joins_and_leaves_a_group_through_mcp(app): from sqlalchemy import select from steward.core.api_tokens import Caller from steward.mcp import _context from steward.mcp.tools import automation as a from steward.models.alerts import AlertOperator, AlertRule from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget from steward.models.audit import AuditEvent from steward.models.hosts import Host from steward.models.users import User, UserRole tag = uuid.uuid4().hex[:8] uid, username = str(uuid.uuid4()), f"gm-op-{tag}" group, host = f"gm-group-{tag}", f"gm-host-{tag}" async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): s.add(User(id=uid, username=username, email=f"{tag}@e.test", password_hash="x", role=UserRole.operator, is_active=True)) h = Host(name=host, address="10.9.9.9") s.add(h) await s.flush() s.add(AnsibleTarget(name=f"gm-target-{tag}", address="10.9.9.9", host_id=h.id)) s.add(AnsibleTarget(name=f"gm-other-{tag}", address="10.9.9.8")) s.add(AnsibleGroup(name=group)) s.add(AlertRule(name=f"gm-rule-{tag}", source_module="host_agent", resource_name="", metric_name="cpu_pct", operator=AlertOperator.gt, threshold=90.0, covers="group", cover_group=group, created_by=uid)) _context._app_ctx.set(app) _context._caller_ctx.set(Caller(user_id=uid, username=username, role=UserRole.operator, token_id=7)) got = await a.update_group_members(group, add=[host, f"gm-other-{tag}"]) assert got["targets"] == sorted([f"gm-other-{tag}", f"gm-target-{tag}"]) assert got["added"] == got["targets"] and got["alert_rules"] == [f"gm-rule-{tag}"] assert "within a minute" in got["note"] inv = await a.list_inventory() assert next(t for t in inv["targets"] if t["host_name"] == host)["groups"] == [group] # Adding again changes nothing; an unknown name refuses the lot. again = await a.update_group_members(group, add=[f"gm-target-{tag}"]) assert again["added"] == [] and "note" not in again with pytest.raises(ValueError, match="Nothing was changed"): await a.update_group_members(group, remove=[host, "no-such-host"]) got = await a.update_group_members(group, remove=[host]) assert got["removed"] == [f"gm-target-{tag}"] and got["targets"] == [f"gm-other-{tag}"] async with app.db_sessionmaker() as s: rows = list((await s.execute(select(AuditEvent).where( AuditEvent.user_id == uid, AuditEvent.action == "ansible.group.updated") .order_by(AuditEvent.timestamp))).scalars()) assert len(rows) == 2 and rows[0].username == f"{username} via MCP" assert json.loads(rows[1].detail_json)["removed"] == [f"gm-target-{tag}"] asyncio.run(_go())