"""Unit tests — no live Odoo or DB connection required.""" from __future__ import annotations import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from mt_odoo_mcp.confirmation.manager import ConfirmationManager from mt_odoo_mcp.audit.logger import _sanitize # ─── Confirmation Manager ────────────────────────────────────────────────────── def test_token_created(): mgr = ConfirmationManager() pending = mgr.create_token("default", "project.task", 42, "Fix Bug") assert pending.token.startswith("del_") assert pending.model == "project.task" assert pending.record_id == 42 def test_token_consumed(): mgr = ConfirmationManager() pending = mgr.create_token("default", "res.partner", 99, "ACME Corp") ok, err = mgr.consume_token(pending.token, "res.partner", 99) assert ok is True assert err == "" def test_token_single_use(): mgr = ConfirmationManager() pending = mgr.create_token("default", "sale.order", 7, "SO007") mgr.consume_token(pending.token, "sale.order", 7) ok, err = mgr.consume_token(pending.token, "sale.order", 7) assert ok is False assert "expired" in err.lower() or "invalid" in err.lower() def test_token_wrong_model(): mgr = ConfirmationManager() pending = mgr.create_token("default", "project.task", 10, "Task A") ok, err = mgr.consume_token(pending.token, "sale.order", 10) assert ok is False assert "does not match" in err.lower() def test_token_wrong_record_id(): mgr = ConfirmationManager() pending = mgr.create_token("default", "project.task", 10, "Task A") ok, _ = mgr.consume_token(pending.token, "project.task", 999) assert ok is False def test_invalid_token(): mgr = ConfirmationManager() ok, _ = mgr.consume_token("del_notarealtoken", "project.task", 1) assert ok is False def test_list_pending(): mgr = ConfirmationManager() mgr.create_token("default", "account.move", 55, "INV/2026/001") assert any(p["model"] == "account.move" for p in mgr.list_pending()) def test_per_user_isolation(): """Two ConfirmationManager instances must not share tokens.""" mgr_a = ConfirmationManager() mgr_b = ConfirmationManager() pending = mgr_a.create_token("default", "project.task", 1, "Task") ok, _ = mgr_b.consume_token(pending.token, "project.task", 1) assert ok is False # mgr_b has no knowledge of mgr_a's tokens # ─── Audit Logger Sanitization ───────────────────────────────────────────────── def test_sanitize_password(): assert _sanitize({"username": "admin", "password": "s3cret"})["password"] == "***" def test_sanitize_api_key(): result = _sanitize({"api_key": "abc123", "model": "project.task"}) assert result["api_key"] == "***" assert result["model"] == "project.task" def test_sanitize_nested(): result = _sanitize({"instance": {"name": "prod", "api_key": "secret"}}) assert result["instance"]["api_key"] == "***" assert result["instance"]["name"] == "prod" def test_sanitize_list_passthrough(): result = _sanitize({"ids": [1, 2, 3], "token": "tok_abc"}) assert result["ids"] == [1, 2, 3] assert result["token"] == "***" def test_sanitize_plain(): data = {"model": "sale.order", "limit": 10} assert _sanitize(data) == data # ─── Auth Crypto ────────────────────────────────────────────────────────────── def test_password_hash_verify(): from mt_odoo_mcp.auth.crypto import hash_password, verify_password h = hash_password("mysecret123") assert verify_password("mysecret123", h) assert not verify_password("wrong", h) def test_api_key_format(): from mt_odoo_mcp.auth.crypto import generate_api_key full, key_hash, prefix = generate_api_key() assert full.startswith("mtom_") assert len(full) > 20 assert prefix == full[:12] def test_api_key_verify(): from mt_odoo_mcp.auth.crypto import generate_api_key, verify_api_key full, key_hash, _ = generate_api_key() assert verify_api_key(full, key_hash) assert not verify_api_key("mtom_wrong", key_hash)