"""Tests para hoppscotch_set_environment. Deterministas: monkeypatchean requests.post (capa de red) y pass_get_secret. Verifican crear vs actualizar (idempotencia por nombre), resolucion de secretos `pass:` (fuerza secret=True, key en resolved_secrets, valor crudo fuera del output), y abortar sin llamar la mutation si pass falla. Hay un test e2e real marcado skip por defecto (self-host vivo). """ import json import sys import pytest import infra.hoppscotch_set_environment # noqa: F401 # El __init__ rebinds el nombre a la funcion; recuperamos el submodulo real. mod = sys.modules["infra.hoppscotch_set_environment"] class _FakeResponse: def __init__(self, status_code=200, json_data=None): self.status_code = status_code self._json = json_data def json(self): if self._json is None: raise ValueError("no json") return self._json def _make_post(call_log, list_envs, mutation_result): """Construye un fake requests.post que distingue listado de mutation. El listado lleva 'teamEnvironments' en la query; cualquier otra es mutation. Cada llamada se registra en call_log para inspeccion. """ def fake_post(url, **kwargs): payload = kwargs["json"] query = payload["query"] call_log.append({"url": url, "kwargs": kwargs, "query": query}) if "teamEnvironments" in query: return _FakeResponse( 200, {"data": {"team": {"teamEnvironments": list_envs}}} ) # mutation (create o update) field = ( "updateTeamEnvironment" if "updateTeamEnvironment" in query else "createTeamEnvironment" ) return _FakeResponse(200, {"data": {field: mutation_result}}) return fake_post def test_crea_cuando_no_existe(monkeypatch): calls = [] monkeypatch.setattr( mod.requests, "post", _make_post(calls, list_envs=[], mutation_result={"id": "env-1", "name": "test_env"}), ) result = mod.hoppscotch_set_environment( "team-1", "test_env", [{"key": "foo", "value": "bar", "secret": False}], access_token="ACCESS-JWT", ) assert result["status"] == "ok" assert result["id"] == "env-1" assert result["action"] == "created" assert result["resolved_secrets"] == [] # Segunda llamada = mutation createTeamEnvironment con las variables. mutation = calls[1] assert "createTeamEnvironment" in mutation["query"] assert mutation["kwargs"]["cookies"] == {"access_token": "ACCESS-JWT"} gql_vars = mutation["kwargs"]["json"]["variables"] assert gql_vars["n"] == "test_env" assert gql_vars["t"] == "team-1" sent_vars = json.loads(gql_vars["v"]) assert sent_vars == [{"key": "foo", "value": "bar", "secret": False}] def test_actualiza_cuando_existe(monkeypatch): calls = [] monkeypatch.setattr( mod.requests, "post", _make_post( calls, list_envs=[{"id": "env-existing", "name": "test_env"}], mutation_result={"id": "env-existing", "name": "test_env"}, ), ) result = mod.hoppscotch_set_environment( "team-1", "test_env", [{"key": "foo", "value": "bar"}], access_token="A", ) assert result["status"] == "ok" assert result["id"] == "env-existing" assert result["action"] == "updated" mutation = calls[1] assert "updateTeamEnvironment" in mutation["query"] gql_vars = mutation["kwargs"]["json"]["variables"] assert gql_vars["id"] == "env-existing" def test_resuelve_secreto_desde_pass(monkeypatch): calls = [] monkeypatch.setattr( mod.requests, "post", _make_post(calls, list_envs=[], mutation_result={"id": "env-2", "name": "e"}), ) def fake_pass(path, **kwargs): assert path == "apis/lpd" return {"status": "ok", "value": "TOP-SECRET-VALUE"} monkeypatch.setattr(mod, "pass_get_secret", fake_pass) result = mod.hoppscotch_set_environment( "team-1", "e", [ {"key": "plain", "value": "visible", "secret": False}, {"key": "apikey", "value": "pass:apis/lpd", "secret": False}, ], access_token="A", ) assert result["status"] == "ok" # El key resuelto aparece, pero NUNCA el valor crudo. assert result["resolved_secrets"] == ["apikey"] assert "TOP-SECRET-VALUE" not in json.dumps(result) # La variable resuelta viaja con el valor real y secret=True forzado. mutation = calls[1] sent_vars = json.loads(mutation["kwargs"]["json"]["variables"]["v"]) by_key = {v["key"]: v for v in sent_vars} assert by_key["apikey"]["value"] == "TOP-SECRET-VALUE" assert by_key["apikey"]["secret"] is True assert by_key["plain"]["value"] == "visible" assert by_key["plain"]["secret"] is False def test_error_pass_no_llama_mutation(monkeypatch): calls = [] def fake_post(url, **kwargs): calls.append(kwargs["json"]["query"]) return _FakeResponse(200, {"data": {"team": {"teamEnvironments": []}}}) monkeypatch.setattr(mod.requests, "post", fake_post) def fake_pass(path, **kwargs): return {"status": "error", "error": "pass not installed"} monkeypatch.setattr(mod, "pass_get_secret", fake_pass) result = mod.hoppscotch_set_environment( "team-1", "e", [{"key": "apikey", "value": "pass:apis/lpd"}], access_token="A", ) assert result["status"] == "error" assert "apikey" in result["error"] # No se hizo ninguna llamada de red (ni listado ni mutation): aborta antes. assert calls == [] @pytest.mark.skip(reason="e2e real contra self-host vivo") def test_e2e_create_then_update_live(): """End-to-end real contra el Hoppscotch self-host vivo. login -> set_environment("test_env") -> created -> set_environment de nuevo -> updated. Limpia el env al final con deleteTeamEnvironment. """ sys.path.insert(0, "python/functions") from infra.hoppscotch_login import hoppscotch_login import requests team_id = "cmq8kn0v500030xls1nvminjy" token = hoppscotch_login("admin@example.com")["access_token"] first = mod.hoppscotch_set_environment( team_id, "test_env", [{"key": "foo", "value": "bar", "secret": False}], access_token=token, ) assert first["status"] == "ok" assert first["action"] == "created" env_id = first["id"] second = mod.hoppscotch_set_environment( team_id, "test_env", [{"key": "foo", "value": "baz", "secret": False}], access_token=token, ) assert second["status"] == "ok" assert second["action"] == "updated" assert second["id"] == env_id # Cleanup: borra el env de prueba. del_q = "mutation($id:ID!){ deleteTeamEnvironment(id:$id) }" requests.post( "http://localhost:3170/graphql", json={"query": del_q, "variables": {"id": env_id}}, cookies={"access_token": token}, timeout=15.0, )