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()