diff --git a/tests/patched/idty_tools.py b/tests/patched/idty_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..fd795275840f3fdfb1257988e93c208f4fbeccdf --- /dev/null +++ b/tests/patched/idty_tools.py @@ -0,0 +1,128 @@ +# Copyright 2016-2021 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/>. + +from duniterpy.documents.block_id import BlockID +from duniterpy.documents.identity import Identity + +idty1 = Identity( + currency="g1-test", + pubkey="6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", + uid="Claude", + block_id=BlockID.from_str( + "597334-002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD" + ), +) +idty1.signature = "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==" + +lookup_one = { + "partial": False, + "results": [ + { + "pubkey": "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", + "uids": [ + { + "uid": "Claude", + "meta": { + "timestamp": "597334-002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD" + }, + "revoked": False, + "revoked_on": None, + "revocation_sig": None, + "self": "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==", + "others": [], + }, + ], + }, + ], +} + + +lookup_two = { + "partial": False, + "results": [ + { + "pubkey": "6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o", + "uids": [ + { + "uid": "Claude", + "meta": { + "timestamp": "597334-002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD" + }, + "revoked": False, + "revoked_on": None, + "revocation_sig": None, + "self": "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==", + "others": [], + }, + { + "uid": "Claudia", + "meta": { + "timestamp": "597334-002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD" + }, + "revoked": False, + "revoked_on": None, + "revocation_sig": None, + "self": "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==", + "others": [], + }, + ], + }, + ], +} + + +idty2 = Identity( + currency="g1-test", + pubkey="969qRJs8KhsnkyzqarpL4RKZGMdVKNbZgu8fhsigM7Lj", + uid="aa_aa", + block_id=BlockID.from_str( + "703902-00002D6BC5E4FC540A4E188C3880A0ACCA06CD77017D26231A515312162B4070" + ), +) +idty2.signature = "3RNQcKNI1VMmuCpK7wer8haOA959EQSDIR1v0Ue/7TpTCOmsU2zYCpC+tqgLQFxDX4A79sB61c11J5C/3Z/TCw==" + + +idty_block = { + "version": 12, + "nonce": 10100000000525, + "number": 597334, + "powMin": 45, + "time": 1594980185, + "medianTime": 1594978717, + "membersCount": 7, + "monetaryMass": 2157680864, + "unitbase": 3, + "issuersCount": 3, + "issuersFrame": 16, + "issuersFrameVar": 0, + "currency": "g1-test", + "issuer": "3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj", + "signature": "Eme9mi25DtUQrP3Hk6evJBQP6GRU0asJrl9G2RWUgtB71AMOWqs/NeraG8YBwQEGokQg1mHMUv7fEoUiEetwDw==", + "hash": "002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD", + "parameters": "", + "previousHash": "0023B87885C52CDE75694C71BED1237B5C7B686C00AB68C8D75693513E1F8765", + "previousIssuer": "39YyHCMQNmXY7NkPCXXfzpV1vYct4GBxwgfyd4d72HmB", + "inner_hash": "46D99F8431053892F230E4E07EC16A2A68B09D68EBC3F9FD796289493AFAFFB5", + "dividend": None, + "identities": [], + "joiners": [], + "actives": [], + "leavers": [], + "revoked": [], + "excluded": [], + "certifications": [], + "transactions": [], + "raw": "Version: 12\nType: Block\nCurrency: g1-test\nNumber: 597334\nPoWMin: 45\nTime: 1594980185\nMedianTime: 1594978717\nUnitBase: 3\nIssuer: 3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj\nIssuersFrame: 16\nIssuersFrameVar: 0\nDifferentIssuersCount: 3\nPreviousHash: 0023B87885C52CDE75694C71BED1237B5C7B686C00AB68C8D75693513E1F8765\nPreviousIssuer: 39YyHCMQNmXY7NkPCXXfzpV1vYct4GBxwgfyd4d72HmB\nMembersCount: 7\nIdentities:\nJoiners:\nActives:\nLeavers:\nRevoked:\nExcluded:\nCertifications:\nTransactions:\nInnerHash: 46D99F8431053892F230E4E07EC16A2A68B09D68EBC3F9FD796289493AFAFFB5\nNonce: 10100000000525\n", +} diff --git a/tests/test_revocation.py b/tests/test_revocation.py new file mode 100644 index 0000000000000000000000000000000000000000..afc9fafc306e19b8227a49ec42e86d61cdb8bf56 --- /dev/null +++ b/tests/test_revocation.py @@ -0,0 +1,1107 @@ +# Copyright 2016-2021 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 os +import sys +import urllib +from pathlib import Path +from unittest.mock import Mock + +import click +import pytest +from click.testing import CliRunner +from duniterpy.api import bma +from duniterpy.api.client import Client +from duniterpy.api.errors import DuniterError +from duniterpy.documents.identity import Identity +from duniterpy.documents.revocation import Revocation + +from patched.auth import patched_auth_method +from patched.blockchain_tools import patched_head_block, patched_head_block_gtest +from patched.idty_tools import idty1, idty2, idty_block, lookup_one, lookup_two +from silkaj import auth, idty_tools, revocation, wot +from silkaj.blockchain_tools import HeadBlock +from silkaj.cli import cli +from silkaj.constants import FAILURE_EXIT_STATUS, SUCCESS_EXIT_STATUS +from silkaj.network_tools import ClientInstance +from silkaj.tui import display_pubkey_and_checksum + +### useful function ### + + +def assertEqual(test: Revocation, expected: Revocation): + """ + Checks the equality of two revocation objects. + Should be replaced by 'Revoc1 == Revoc2' once __eq__() method has been published in Duniterpy. + """ + # direct equality fails, I can't find why. + # assert test == expected + # so, let's test all values. + assert test.version == expected.version + assert test.currency == expected.currency + assert test.pubkey == expected.pubkey + # fields for identity + assert test.identity.version == expected.identity.version + assert test.identity.currency == expected.identity.currency + assert test.identity.pubkey == expected.identity.pubkey + assert test.identity.uid == expected.identity.uid + assert test.identity.block_id == expected.identity.block_id + assert test.identity.signature == expected.identity.signature + # test raw and inline are the same + assert test.raw() == expected.raw() + assert test.inline() == expected.inline() + + +def display_dry_options(display, dry_run): + if display: + return ["--display"] + elif dry_run: + return ["--dry-run"] + else: + return [] + + +### values ### + +# idty1 + +REV_DOC = Revocation( + version=10, + currency="g1-test", + identity=idty1, +) +REV_DOC.signature = "dTv6HHnyBsMXofOZFT21Y3gRzG/frseCaZFfpatpCWjYsNA8HPHjTibLUcJ3E9ZUgd0QUV7Bbu868xQE+j/yAg==" + +REV_DOC_FALSE = Revocation( + version=10, + currency="g1-test", + identity=idty1, +) +REV_DOC_FALSE.signature = "XXXXXXXXBsMXofOZFT21Y3gRzG/frseCaZFfpatpCWjYsNA8HPHjTibLUcJ3E9ZUgd0QUV7Bbu868xQE+j/yAg==" + + +# idty2 + +REV_2 = Revocation( + version=10, + currency="g1-test", + identity=idty2, +) +REV_2.signature = "42D2vbIJnv2aGqUMbD+BF+eChzzGo4R3CVPAl5hpIGvoTcZQCfKBsRRlZDx6Gwn6lsJ3KLiIwPQeJKGYCW2YBg==" + +REV_2_FALSE = Revocation( + version=10, + currency="g1-test", + identity=idty2, +) +REV_2_FALSE.signature = "XXXXXXIJnv2aGqUMbD+BF+eChzzGo4R3CVPAl5hpIGvoTcZQCfKBsRRlZDx6Gwn6lsJ3KLiIwPQeJKGYCW2YBg==" + + +WRONG_FORMAT_REV = "ersion: 10\ +Type: Revocation\ +Currency: g1-test\ +Issuer: 969qRJs8KhsnkyzqarpL4RKZGMdVKNbZgu8fhsigM7Lj\ +IdtyUniqueID: aa_aa\ +IdtyTimestamp: 703902-00002D6BC5E4FC540A4E188C3880A0ACCA06CD77017D26231A515312162B4070\ +IdtySignature: 3RNQcKNI1VMmuCpK7wer8haOA959EQSDIR1v0Ue/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, doc_type): + return True + + +def patched_choose_identity(pubkey): + return ( + { + "uid": idty1.uid, + "meta": {"timestamp": str(idty1.block_id)}, + "self": "kFW2we2K3zx4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==", + }, + 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", + [ + ( + "save", + 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(HeadBlock, "get_head", patched_head_block_gtest) + monkeypatch.setattr(wot, "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", "revocation", subcommand] + print("command: ", " ".join(command)) # debug + file = "revocation.txt" + runner = CliRunner() + with runner.isolated_filesystem(): + with open(file, "w") as f: + f.write(REV_DOC.signed_raw()) + 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 not warning in result.output + + +# test cli save +@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(HeadBlock, "get_head", patched_head_block_gtest) + monkeypatch.setattr(wot, "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 = ["revocation", "save"] + 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(HeadBlock, "get_head", patched_head_block_gtest) + + # prepare command + command = display_dry_options(display, dry_run) + command.extend(["revocation", "verify"]) + if file: + command.extend([file]) + else: + file = revocation.REVOCATION_LOCAL_PATH + + # verify file + runner = CliRunner() + with runner.isolated_filesystem(): + with open(file, "w") as f: + f.write(doc.signed_raw()) + result = runner.invoke(cli, args=command) + for expect in expected: + assert expect in result.output + for not_expect in not_expected: + assert not not_expect 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 lookup == False: + 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(HeadBlock, "get_head", patched_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(["revocation", "publish"]) + if file: + command.extend([file]) + else: + file = revocation.REVOCATION_LOCAL_PATH + + # test publication + client = ClientInstance().client + runner = CliRunner() + with runner.isolated_filesystem(): + with open(file, "w") as f: + f.write(doc.signed_raw()) + 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, 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(HeadBlock, "get_head", patched_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(["revocation", "publish"]) + if file: + command.extend([file]) + else: + file = revocation.REVOCATION_LOCAL_PATH + + # test publication + client = ClientInstance().client + runner = CliRunner() + with runner.isolated_filesystem(): + with open(file, "w") as f: + f.write(REV_DOC.signed_raw()) + 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(HeadBlock, "get_head", patched_head_block_gtest) + monkeypatch.setattr(wot, "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(["revocation", "revoke"]) + client = ClientInstance().client + + 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(HeadBlock, "get_head", patched_head_block_gtest) + monkeypatch.setattr(wot, "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(["revocation", "revoke"]) + client = ClientInstance().client + + 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, + ) + assertEqual(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() + with open(path) as f: + assert f.read() == 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: {display_pubkey_and_checksum(pubkey)}" + assert expected_confirm in capsys.readouterr().out + with open(path) as f: + assert f.read() == 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 + with open(path) as f: + assert f.read() == 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(): + with open(path, "w") as f: + f.write(doc.signed_raw()) + 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 + assertEqual(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 lookup == False: + 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(): + with open(path, "w") as f: + f.write(doc.signed_raw()) + with pytest.raises(SystemExit) as pytest_exit: + result = revocation.verify_document(path) + assert pytest_exit.type == SystemExit + display = capsys.readouterr().out + if lookup == False: + 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(): + with open(path, "w") as f: + if isinstance(doc, str): + f.write(doc) + elif isinstance(doc, Revocation): + f.write(doc.signed_raw()) + with pytest.raises(SystemExit) as pytest_exit: + result = 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()