# Copyright 2016-2023 Maƫl Azimi <m.a@moul.re> # # Silkaj is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Silkaj is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Silkaj. If not, see <https://www.gnu.org/licenses/>. import urllib from pathlib import Path from unittest.mock import Mock import pytest import rich_click as click from click.testing import CliRunner from duniterpy.api import bma from duniterpy.documents.revocation import Revocation from silkaj import auth from silkaj.blockchain import tools as bc_tools from silkaj.cli import cli from silkaj.constants import FAILURE_EXIT_STATUS, SUCCESS_EXIT_STATUS from silkaj.network import client_instance from silkaj.public_key import gen_pubkey_checksum from silkaj.wot import idty_tools, revocation from silkaj.wot import tools as w_tools from tests.patched.auth import patched_auth_method from tests.patched.blockchain_tools import patched_get_head_block_gtest from tests.patched.idty_tools import idty1, idty2, idty_block, lookup_one, lookup_two # Useful function def display_dry_options(display, dry_run): if display: return ["--display"] if dry_run: return ["--dry-run"] return [] # Values # idty1 REV_DOC = Revocation( version=10, currency="g1-test", identity=idty1, ) REV_DOC.signature = "dTv6HHnyBsMXofOZFT21Y3gRzG/frseCaZFfpatpCWj\ YsNA8HPHjTibLUcJ3E9ZUgd0QUV7Bbu868xQE+j/yAg==" REV_DOC_FALSE = Revocation( version=10, currency="g1-test", identity=idty1, ) REV_DOC_FALSE.signature = "XXXXXXXXBsMXofOZFT21Y3gRzG/frseCaZFfp\ atpCWjYsNA8HPHjTibLUcJ3E9ZUgd0QUV7Bbu868xQE+j/yAg==" # idty2 REV_2 = Revocation( version=10, currency="g1-test", identity=idty2, ) REV_2.signature = "42D2vbIJnv2aGqUMbD+BF+eChzzGo4R3CVPAl5hpIGvoT\ cZQCfKBsRRlZDx6Gwn6lsJ3KLiIwPQeJKGYCW2YBg==" REV_2_FALSE = Revocation( version=10, currency="g1-test", identity=idty2, ) REV_2_FALSE.signature = "XXXXXXIJnv2aGqUMbD+BF+eChzzGo4R3CVPAl5hp\ IGvoTcZQCfKBsRRlZDx6Gwn6lsJ3KLiIwPQeJKGYCW2YBg==" WRONG_FORMAT_REV = "ersion: 10\ Type: Revocation\ Currency: g1-test\ Issuer: 969qRJs8KhsnkyzqarpL4RKZGMdVKNbZgu8fhsigM7Lj\ IdtyUniqueID: aa_aa\ IdtyTimestamp: 703902-00002D6BC5E4FC540A4E188C3880A0ACCA06CD77017D26231A515312162B4070\ IdtySignature: 3RNQcKNI1VMmuCpK7wer8haOA959EQSDIR1v0U\ e/7TpTCOmsU2zYCpC+tqgLQFxDX4A79sB61c11J5C/3Z/TCw==\ 42D2vbIJnv2aGqUMbD+BF+eChzzGo4R3CVPAl5hpIGvoTcZQCfKBsRRlZDx6Gwn6lsJ3KLiIwPQeJKGYCW2YBg==" ERROR_CODE = 1005 ERROR_MESSAGE = "Document has unkown fields or wrong line ending format" # patched functions def patch_get_id_block(node, number): return idty_block def patched_auth_method_Claude(): return patched_auth_method("a") def patch_check_many_identities(idty): return True def patched_choose_identity(pubkey): return ( { "uid": idty1.uid, "meta": {"timestamp": str(idty1.block_id)}, "self": "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09\ Gjsttl0i9GtWzodyJ6mBE1q7jcAw==", }, idty1.pubkey, None, ) def patched_send_bma_revoke_error(wot_useless, rev_doc_useless): raise urllib.error.HTTPError( url="this/is/a/test.url", code=ERROR_CODE, msg=ERROR_MESSAGE, hdrs={}, fp=None, ) # tests # test cli dry-run @pytest.mark.parametrize( ("subcommand", "expected_warn"), [ ( "create", False, ), ( "verify", False, ), ( "publish", True, ), ( "revoke", True, ), ], ) def test_revocation_cli_dry_run(subcommand, expected_warn, monkeypatch): """ Tests dry-run option behavior when associated with other options """ monkeypatch.setattr(auth, "auth_method", patched_auth_method_Claude) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(w_tools, "choose_identity", patched_choose_identity) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) monkeypatch.setattr( idty_tools, "check_many_identities", patch_check_many_identities, ) print("subcommand: ", subcommand) # debug warning = "WARNING: the document will only be displayed and will not be sent." command = ["--dry-run", "-gt", "wot", "revocation", subcommand] print("command: ", " ".join(command)) # debug file = "revocation.txt" runner = CliRunner() with runner.isolated_filesystem(): Path(file).write_text(REV_DOC.signed_raw(), encoding="utf-8") result = runner.invoke(cli, args=command) assert "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o" in result.output assert "Version: 10" in result.output if expected_warn: assert warning in result.output else: assert warning not in result.output # test cli create @pytest.mark.parametrize( ("display", "dry_run", "file", "user_input", "expected"), [ ( False, False, "", "yes\n", [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", ], ), ( True, False, "", "yes\n", [ "Version: 10", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", ], ), ( False, True, "", None, [ "Version: 10", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", ], ), ( False, False, "test_doc", "yes\n", [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", ], ), ( True, False, "test_doc", "yes\n", [ "Version: 10", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", ], ), ( False, True, "test_doc", None, [ "Version: 10", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", ], ), ( False, False, "", "no\n", [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", "Ok, goodbye!", ], ), ( True, False, "", "no\n", [ "Version: 10", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you want to save the revocation document for this identity?", "Ok, goodbye!", ], ), ], ) def test_revocation_cli_save(display, dry_run, file, user_input, expected, monkeypatch): monkeypatch.setattr(auth, "auth_method", patched_auth_method_Claude) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(w_tools, "choose_identity", patched_choose_identity) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) patched_save_doc = Mock() monkeypatch.setattr( revocation, "save_doc", patched_save_doc, ) command = display_dry_options(display, dry_run) subcommand = ["wot", "revocation", "create"] command.extend(subcommand) if file: command.extend([file]) else: file = revocation.REVOCATION_LOCAL_PATH result = CliRunner().invoke(cli, args=command, input=user_input) if not dry_run and user_input == "yes\n": patched_save_doc.assert_called_with(file, REV_DOC.signed_raw(), idty1.pubkey) elif user_input == "no\n" or dry_run: patched_save_doc.assert_not_called() for expect in expected: assert expect in result.output # test cli verify @pytest.mark.parametrize( ("display", "dry_run", "doc", "lookup", "file", "expected", "not_expected"), [ ( False, False, REV_DOC, lookup_one, "", [ "| Public key |", "Revocation document is valid.\n", ], ["Version: 10"], ), ( False, False, REV_DOC, lookup_two, "", [ "One matching identity!\nSimilar identities:", "uid", "| Public key |", "Revocation document is valid.\n", ], ["Version: 10"], ), ( True, False, REV_DOC, lookup_one, "", [ "Version: 10", "| Public key |", "Revocation document is valid.\n", ], [], ), ( False, True, REV_DOC, lookup_one, "", [ "Version: 10", ], [ "Revocation document is valid.\n", ], ), ( False, False, REV_DOC_FALSE, lookup_one, "", ["Error: the signature of the revocation document is invalid."], [ "Version: 10", "| Public key |", "Revocation document is valid.\n", ], ), ( False, False, REV_2, lookup_two, "test_doc", [ "Revocation document does not match any valid identity.\nSimilar identities:", "uid", "Claude", "Claudia", ], [ "Revocation document is valid.\n", "| Public key |", "Version: 10", ], ), ], ) def test_revocation_cli_verify( display, dry_run, doc, lookup, file, expected, not_expected, monkeypatch, ): def patched_lookup(node, id_pubkey): return lookup monkeypatch.setattr(bma.wot, "lookup", patched_lookup) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) # prepare command command = display_dry_options(display, dry_run) command.extend(["wot", "revocation", "verify"]) if file: command.extend([file]) else: file = revocation.REVOCATION_LOCAL_PATH # verify file runner = CliRunner() with runner.isolated_filesystem(): Path(file).write_text(doc.signed_raw(), encoding="utf-8") result = runner.invoke(cli, args=command) for expect in expected: assert expect in result.output for not_expect in not_expected: assert not_expect not in result.output # test cli publish @pytest.mark.parametrize( ("display", "dry_run", "doc", "lookup", "file", "user_input", "expected"), [ ( False, False, REV_DOC, lookup_one, "", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", ], ), ( False, False, REV_DOC, lookup_two, "", "yes\n", [ "One matching identity!\nSimilar identities:", "Claudia", "| Public key |", "Do you confirm sending this revocation document immediately?", ], ), ( True, False, REV_DOC, lookup_one, "", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Version: 10", ], ), ( True, False, REV_DOC, lookup_two, "", "yes\n", [ "One matching identity!\nSimilar identities:", "| Public key |", "Do you confirm sending this revocation document immediately?", "Version: 10", ], ), ( False, True, REV_DOC, lookup_one, "", None, [ "WARNING: the document will only be displayed and will not be sent.", "Version: 10", ], ), ( False, True, REV_DOC, lookup_two, "", None, [ "One matching identity!\nSimilar identities:", "WARNING: the document will only be displayed and will not be sent.", "Version: 10", ], ), ( False, False, REV_DOC, lookup_one, "", "no\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", ], ), ( True, False, REV_DOC, lookup_one, "", "no\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Version: 10", ], ), ( False, False, REV_DOC_FALSE, lookup_one, "", None, ["Error: the signature of the revocation document is invalid."], ), ( True, False, REV_DOC_FALSE, lookup_one, "", None, ["Error: the signature of the revocation document is invalid."], ), ( False, True, REV_DOC_FALSE, lookup_one, "", None, [ "WARNING: the document will only be displayed and will not be sent.", "Error: the signature of the revocation document is invalid.", ], ), ( False, False, REV_DOC, lookup_one, "test_doc", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", ], ), ( True, False, REV_DOC, lookup_one, "test_doc", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", ], ), ( False, True, REV_DOC, lookup_one, "test_doc", "yes\n", [ "WARNING: the document will only be displayed and will not be sent.", "Version: 10", ], ), ( False, False, REV_DOC_FALSE, lookup_one, "test_doc", None, ["Error: the signature of the revocation document is invalid."], ), ( True, False, REV_DOC_FALSE, lookup_one, "test_doc", None, ["Error: the signature of the revocation document is invalid."], ), ( False, True, REV_DOC_FALSE, lookup_one, "test_doc", None, [ "Error: the signature of the revocation document is invalid.", "WARNING: the document will only be displayed and will not be sent.", ], ), ( False, False, REV_2, lookup_two, "", "", [ "Revocation document does not match any valid identity.\nSimilar identities:", ], ), ( False, False, REV_DOC, False, "", "", ["Revocation document does not match any valid identity."], ), ], ) def test_revocation_cli_publish( display, dry_run, doc, lookup, file, user_input, expected, monkeypatch, ): def patched_lookup(node, id_pubkey): if not lookup: raise urllib.error.HTTPError( url="this/is/a/test.url", code=404, msg="(Test) Not Found", hdrs={}, fp=None, ) return lookup monkeypatch.setattr(bma.wot, "lookup", patched_lookup) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) patched_send_bma_revoke = Mock() monkeypatch.setattr(bma.wot, "revoke", patched_send_bma_revoke) # prepare command command = display_dry_options(display, dry_run) command.extend(["wot", "revocation", "publish"]) if file: command.extend([file]) else: file = revocation.REVOCATION_LOCAL_PATH # test publication runner = CliRunner() with runner.isolated_filesystem(): Path(file).write_text(doc.signed_raw(), encoding="utf-8") result = runner.invoke(cli, args=command, input=user_input) if user_input == "yes\n" and not dry_run: patched_send_bma_revoke.assert_called_once_with( client_instance(), doc.signed_raw(), ) elif dry_run or user_input == "no\n": patched_send_bma_revoke.assert_not_called() for expect in expected: assert expect in result.output # test cli publish send errors @pytest.mark.parametrize( ("display", "file", "user_input", "expected"), [ ( False, "", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Error while publishing revocation", ], ), ( True, "", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Version: 10", "Error while publishing revocation", ], ), ( False, "test_doc", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Error while publishing revocation", ], ), ( True, "test_doc", "yes\n", [ "| Public key |", "Do you confirm sending this revocation document immediately?", "Version: 10", "Error while publishing revocation", ], ), ], ) def test_revocation_cli_publish_send_errors( display, file, user_input, expected, monkeypatch, ): def patched_lookup(node, id_pubkey): return lookup_one monkeypatch.setattr(bma.wot, "lookup", patched_lookup) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) monkeypatch.setattr(bma.wot, "revoke", patched_send_bma_revoke_error) # prepare command command = display_dry_options(display, False) command.extend(["wot", "revocation", "publish"]) if file: command.extend([file]) else: file = revocation.REVOCATION_LOCAL_PATH # test publication runner = CliRunner() with runner.isolated_filesystem(): Path(file).write_text(REV_DOC.signed_raw(), encoding="utf-8") result = runner.invoke(cli, args=command, input=user_input) for expect in expected: assert expect in result.output assert result.exit_code == FAILURE_EXIT_STATUS # test cli revoke @pytest.mark.parametrize( ("display", "dry_run", "user_input", "doc", "expected"), [ ( False, False, "yes\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", ], ), ( True, False, "yes\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", "Version: 10", ], ), ( False, True, None, REV_DOC, [ "WARNING: the document will only be displayed and will not be sent.", "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Version: 10", ], ), ( False, False, "no\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", ], ), ( True, False, "no\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", "Version: 10", ], ), ], ) def test_revocation_cli_revoke( display, dry_run, user_input, doc, expected, monkeypatch, ): monkeypatch.setattr(auth, "auth_method", patched_auth_method_Claude) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(w_tools, "choose_identity", patched_choose_identity) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) patched_send_bma_revoke = Mock() monkeypatch.setattr(bma.wot, "revoke", patched_send_bma_revoke) command = display_dry_options(display, dry_run) command.extend(["wot", "revocation", "revoke"]) client = client_instance() result = CliRunner().invoke(cli, args=command, input=user_input) for expect in expected: assert expect in result.output if not dry_run and user_input == "yes\n": patched_send_bma_revoke.assert_called_once_with(client, doc.signed_raw()) if dry_run or user_input == "no\n": patched_send_bma_revoke.assert_not_called() # test cli revoke errors @pytest.mark.parametrize( ("display", "user_input", "doc", "expected"), [ ( False, "yes\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", "Error while publishing revocation", ], ), ( True, "yes\n", REV_DOC, [ "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "Do you confirm sending this revocation document immediately?", "Version: 10", "Error while publishing revocation", ], ), ], ) def test_revocation_cli_revoke_errors(display, user_input, doc, expected, monkeypatch): monkeypatch.setattr(auth, "auth_method", patched_auth_method_Claude) monkeypatch.setattr(bc_tools, "get_head_block", patched_get_head_block_gtest) monkeypatch.setattr(w_tools, "choose_identity", patched_choose_identity) monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block) monkeypatch.setattr(bma.wot, "revoke", patched_send_bma_revoke_error) command = display_dry_options(display, False) command.extend(["wot", "revocation", "revoke"]) result = CliRunner().invoke(cli, args=command, input=user_input) for expect in expected: assert expect in result.output assert result.exit_code == FAILURE_EXIT_STATUS # test create_revocation_doc @pytest.mark.parametrize(("idty", "lookup"), [(idty1, lookup_one)]) def test_create_revocation_doc(idty, lookup): test = revocation.create_revocation_doc( lookup["results"][0]["uids"][0], "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", "g1-test", ) expected = Revocation( version=10, currency="g1-test", identity=idty, ) assert test == expected # test save_doc @pytest.mark.parametrize( ("path", "rev_1", "rev_2", "pubkey"), [ ( "./test_doc.txt", REV_DOC, REV_2, "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", ), ("revocation", REV_DOC, REV_2, "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o"), ], ) def test_save_doc(path, rev_1, rev_2, pubkey, capsys, monkeypatch): def conf_true(confirm_question): return True def conf_false(confirm_question): return False runner = CliRunner() with runner.isolated_filesystem(): # test file is written on en empty location test_path = Path(path) revocation.save_doc(path, rev_1.signed_raw(), pubkey) assert test_path.is_file() assert Path(path).read_text(encoding="utf-8") == rev_1.signed_raw() # test file is overwritten if confirm monkeypatch.setattr(click, "confirm", value=conf_true) revocation.save_doc(path, rev_2.signed_raw(), pubkey) expected_confirm = f"Revocation document file stored into `{path}` \ for following public key: {gen_pubkey_checksum(pubkey)}" assert expected_confirm in capsys.readouterr().out assert Path(path).read_text(encoding="utf-8") == rev_2.signed_raw() # test file is not overwritten if not confirm monkeypatch.setattr(click, "confirm", value=conf_false) with pytest.raises(SystemExit) as pytest_exit: revocation.save_doc(path, rev_1.signed_raw(), pubkey) assert pytest_exit.type == SystemExit assert pytest_exit.value.code == SUCCESS_EXIT_STATUS expected_confirm = "Ok, goodbye!" assert expected_confirm in capsys.readouterr().out assert Path(path).read_text(encoding="utf-8") == rev_2.signed_raw() # test verify_document @pytest.mark.parametrize( ("doc", "lookup"), [ (REV_DOC, lookup_one), (REV_DOC, lookup_two), ], ) def test_verify_document(doc, lookup, capsys, monkeypatch): def patched_lookup(node, id_pubkey): return lookup # prepare test path = "test_file.txt" monkeypatch.setattr(bma.wot, "lookup", patched_lookup) # test runner = CliRunner() with runner.isolated_filesystem(): Path(path).write_text(doc.signed_raw(), encoding="utf-8") result = revocation.verify_document(path) display = capsys.readouterr().out if len(lookup["results"]) > 1: assert "One matching identity!\n" in display assert "Similar identities:" in display assert result == doc # test verify_document: no matching identity @pytest.mark.parametrize( ("doc", "lookup"), [ (REV_2, lookup_one), (REV_2, lookup_two), (REV_2, False), ], ) def test_verify_document_missing_id(doc, lookup, capsys, monkeypatch): def patched_lookup(node, id_pubkey): if not lookup: http_error = urllib.error.HTTPError( url="this.is/a/test/url", code=2001, msg="No matching identity", hdrs={}, fp=None, ) raise http_error return lookup # prepare test path = "test_file.txt" monkeypatch.setattr(bma.wot, "lookup", patched_lookup) # test runner = CliRunner() with runner.isolated_filesystem(): Path(path).write_text(doc.signed_raw(), encoding="utf-8") with pytest.raises(SystemExit) as pytest_exit: revocation.verify_document(path) assert pytest_exit.type == SystemExit display = capsys.readouterr().out if not lookup: assert "Revocation document does not match any valid identity." in str( pytest_exit.value.code, ) else: assert pytest_exit.value.code == FAILURE_EXIT_STATUS assert ( "Revocation document does not match any valid identity.\nSimilar identities:" in display ) # test verify_document : format and sign errors @pytest.mark.parametrize( ("doc", "currency"), [ (REV_DOC_FALSE, REV_DOC_FALSE.currency), (REV_2_FALSE, REV_2_FALSE.currency), (WRONG_FORMAT_REV, "g1-test"), ], ) def test_verify_document_sign_errors(doc, currency, monkeypatch): # prepare test path = "test_file.txt" patched_lookup = Mock() monkeypatch.setattr(bma.wot, "lookup", patched_lookup) # test runner = CliRunner() with runner.isolated_filesystem(): file = Path(path) if isinstance(doc, str): file.write_text(doc, encoding="utf-8") elif isinstance(doc, Revocation): file.write_text(doc.signed_raw(), encoding="utf-8") with pytest.raises(SystemExit) as pytest_exit: revocation.verify_document(path) assert pytest_exit.type == SystemExit if isinstance(doc, str): assert ( "is not a revocation document, or is not correctly formatted." in pytest_exit.value.code ) elif isinstance(doc, Revocation): assert ( pytest_exit.value.code == "Error: the signature of the revocation document is invalid." ) patched_lookup.assert_not_called()