from __future__ import annotations import json import subprocess import sys from pathlib import Path class FakeN8nClient: def __init__(self): self.workflow_calls = [] self.execution_calls = [] def list_workflows(self, *, limit, cursor=None): self.workflow_calls.append(cursor) if cursor is None: return { "data": [ { "id": "wf-1", "name": "orders-daily", "active": True, "updatedAt": "2026-07-18T01:00:00Z", "tags": [{"name": "production"}], "nodes": [ { "name": "Daily", "type": "n8n-nodes-base.scheduleTrigger", "parameters": {"rule": {"interval": [{"field": "days"}]}}, }, { "name": "Orders DB", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "executeQuery"}, "credentials": { "postgres": { "id": "credential-id-must-not-leak", "name": "orders-reader", "password": "secret-must-not-leak", } }, }, ], "connections": {}, } ], "nextCursor": "next-page", } return { "data": [ { "id": "wf-2", "name": "community-node-flow", "active": False, "nodes": [ { "name": "Unknown", "type": "community.exampleNode", "parameters": {"apiKey": "also-must-not-leak"}, } ], "connections": {}, } ] } def list_executions( self, *, workflow_id, limit, cursor=None, include_data=False, ): self.execution_calls.append((workflow_id, cursor, include_data)) if workflow_id == "wf-1": return { "data": [ { "id": "execution-1", "status": "success", "startedAt": "2026-07-17T01:00:00Z", "stoppedAt": "2026-07-17T01:02:00Z", "data": {"password": "execution-secret-must-not-leak"}, } ] } return {"data": []} ROOT = Path(__file__).resolve().parents[1] def test_inventory_script_can_run_as_a_direct_cli_entrypoint(): result = subprocess.run( [ sys.executable, str(ROOT / "scripts" / "inventory_n8n_workflows.py"), "--help", ], cwd=ROOT, capture_output=True, text=True, ) assert result.returncode == 0, result.stderr assert "credential-safe n8n migration inventory" in result.stdout def test_inventory_cli_passes_explicit_timeout_without_flask_context( monkeypatch, tmp_path ): import scripts.inventory_n8n_workflows as inventory captured = {} class RecordingClient: def __init__(self, *, api_url, api_key, timeout): captured.update( api_url=api_url, api_key=api_key, timeout=timeout, ) monkeypatch.setattr(inventory, "N8nClient", RecordingClient) monkeypatch.setattr( inventory, "collect_inventory", lambda client, bindings: inventory.empty_inventory(reason="test"), ) monkeypatch.setenv("N8N_API_URL", "http://localhost:15678") monkeypatch.setenv("N8N_API_KEY", "local-test-key") monkeypatch.setenv("N8N_API_TIMEOUT", "17") monkeypatch.setattr( sys, "argv", [ "inventory_n8n_workflows.py", "--output", str(tmp_path / "inventory.json"), ], ) assert inventory.main() == 0 assert captured == { "api_url": "http://localhost:15678", "api_key": "local-test-key", "timeout": 17, } def test_collect_inventory_paginates_and_never_exports_secret_material(): from scripts.inventory_n8n_workflows import collect_inventory result = collect_inventory( FakeN8nClient(), bindings={"wf-1": "019b06e4-20e1-7cc2-88b5-bc8e28db3285"}, generated_at="2026-07-18T02:00:00Z", ) assert result["schema_version"] == "1.0" assert result["collection_status"] == "complete" assert result["coverage"] == { "workflow_count": 2, "active_count": 1, "recently_executed_count": 1, "blocked_count": 1, "unbound_count": 1, } first = result["workflows"][0] assert first["workflow_id"] == "wf-1" assert first["dataflow_uid"] == "019b06e4-20e1-7cc2-88b5-bc8e28db3285" assert first["node_categories"] == ["sql", "trigger"] assert first["credential_references"] == [ { "node_name": "Orders DB", "credential_type": "postgres", "credential_name": "orders-reader", } ] assert first["recent_execution"]["status_counts"] == {"success": 1} assert first["migration_assessment"]["production_write"] == "unknown" assert first["migration_assessment"]["idempotent"] == "unknown" second = result["workflows"][1] assert "unsupported_node:community.exampleNode" in second["migration_assessment"][ "blockers" ] assert "missing_dataflow_binding" in second["migration_assessment"]["blockers"] serialized = json.dumps(result, ensure_ascii=False) for forbidden in ( "credential-id-must-not-leak", "secret-must-not-leak", "also-must-not-leak", "execution-secret-must-not-leak", ): assert forbidden not in serialized def test_empty_collection_is_explicitly_pending_instead_of_claiming_coverage(): from scripts.inventory_n8n_workflows import empty_inventory result = empty_inventory( reason="N8N_API_KEY is not configured", generated_at="2026-07-18T02:00:00Z", ) assert result["collection_status"] == "pending_live_n8n_access" assert result["coverage"]["workflow_count"] == 0 assert result["collection_error"] == "N8N_API_KEY is not configured"