Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • elmau/silkaj
  • Mr-Djez/silkaj
  • jbar/silkaj
  • clients/python/silkaj
  • Bernard/silkaj
  • cebash/silkaj
  • jytou/silkaj
  • c-geek/silkaj
  • vincentux/silkaj
  • jeanlucdonnadieu/silkaj
  • matograine/silkaj
  • zicmama/silkaj
  • manutopik/silkaj
  • atrax/silkaj
14 results
Select Git revision
Show changes
# Copyright 2016-2025 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 pytest
from silkaj import public_key
from silkaj.constants import SHORT_PUBKEY_SIZE
# test gen_checksum
@pytest.mark.parametrize(
("pubkey", "checksum"),
[
("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv"),
],
)
def test_gen_checksum(pubkey, checksum):
assert checksum == public_key.gen_checksum(pubkey)
# test validate_checksum
@pytest.mark.parametrize(
("pubkey", "checksum", "expected"),
[
("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv", None),
(
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX",
"KA",
"Error: public key 'J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX' \
does not match checksum 'KA'.\nPlease verify the public key.\n",
),
],
)
def test_validate_checksum(pubkey, checksum, expected, capsys):
pubkey_with_ck = f"{pubkey}:{checksum}"
if not expected:
assert pubkey == public_key.validate_checksum(pubkey_with_ck)
else:
with pytest.raises(SystemExit) as pytest_exit:
public_key.validate_checksum(pubkey_with_ck)
assert capsys.readouterr().out == expected
assert pytest_exit.type == SystemExit
# test check_pubkey_format
@pytest.mark.parametrize(
("pubkey", "display_error", "expected"),
[
("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAv", True, True),
("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", True, False),
("Youpi", False, None),
("Youpi", True, "Error: bad format for following public key: Youpi\n"),
],
)
def test_check_pubkey_format(pubkey, display_error, expected, capsys):
if isinstance(expected, str):
with pytest.raises(SystemExit) as pytest_exit:
public_key.check_pubkey_format(pubkey, display_error)
assert capsys.readouterr().out == expected
assert pytest_exit.type == SystemExit
else:
assert expected == public_key.check_pubkey_format(pubkey, display_error)
# test is_pubkey_and_check
@pytest.mark.parametrize(
("uid_pubkey", "expected"),
[
(
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAv",
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX",
),
(
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX",
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX",
),
("Youpi", False),
],
)
def test_is_pubkey_and_check(uid_pubkey, expected):
assert expected == public_key.is_pubkey_and_check(uid_pubkey)
# test is_pubkey_and_check errors
@pytest.mark.parametrize(
("uid_pubkey", "expected"),
[
(
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KA",
"Error: bad format for following public key: \
J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KA",
),
(
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAt",
"Error: Wrong checksum for following public key: \
J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX",
),
],
)
def test_is_pubkey_and_check_errors(uid_pubkey, expected, capsys):
with pytest.raises(SystemExit) as pytest_exit: # noqa: PT012
public_key.is_pubkey_and_check(uid_pubkey)
assert capsys.readouterr() == expected
assert pytest_exit.type == SystemExit
# gen_pubkey_checksum
@pytest.mark.parametrize(
("pubkey", "checksum"),
[
("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv"),
],
)
def test_gen_pubkey_checksum(pubkey, checksum):
assert f"{pubkey}:{checksum}" == public_key.gen_pubkey_checksum(pubkey)
assert (
f"{pubkey[:SHORT_PUBKEY_SIZE]}…:{checksum}"
== public_key.gen_pubkey_checksum(
pubkey,
short=True,
)
)
assert f"{pubkey[:14]}…:{checksum}" == public_key.gen_pubkey_checksum(
pubkey,
short=True,
length=14,
)
# Copyright 2016-2025 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 shutil
import pytest
from texttable import Texttable
from silkaj import tui
def test_create_table():
expected = Texttable(max_width=shutil.get_terminal_size().columns)
expected.add_rows([["one", "two"], ["three", "four"]])
expected.set_chars(tui.VERT_TABLE_CHARS)
test_table = tui.Table().add_rows([["one", "two"], ["three", "four"]])
assert expected.draw() == test_table.draw()
expected.set_deco(expected.HEADER | expected.VLINES | expected.BORDER)
test_table = tui.Table("columns").add_rows([["one", "two"], ["three", "four"]])
assert expected.draw() == test_table.draw()
@pytest.mark.parametrize(
("rows", "header", "expected"),
[
(
[
["three", "o'clock"],
["four", "o'clock"],
["rock", "rock around the clock"],
],
["one", "two"],
"│───────│───────────────────────│\n\
│ one │ two │\n\
│═══════│═══════════════════════│\n\
│ three │ o'clock │\n\
│───────│───────────────────────│\n\
│ four │ o'clock │\n\
│───────│───────────────────────│\n\
│ rock │ rock around the clock │\n\
│───────│───────────────────────│",
),
(
[
["three", "o'clock"],
["four", "o'clock"],
["rock", "rock around the clock"],
],
None,
"│───────│───────────────────────│\n\
│ three │ o'clock │\n\
│───────│───────────────────────│\n\
│ four │ o'clock │\n\
│───────│───────────────────────│\n\
│ rock │ rock around the clock │\n\
│───────│───────────────────────│",
),
(
[
["three", "o'clock"],
["four", "o'clock"],
["rock"],
],
["one", "two"],
False,
),
],
)
def test_fill_rows(rows, header, expected):
table = tui.Table()
if not expected:
with pytest.raises(AssertionError):
table.fill_rows(rows, header)
else:
table.fill_rows(rows, header)
assert table.draw() == expected
@pytest.mark.parametrize(
("dict_", "expected"),
[
(
{
"one": ["three", "four", "rock"],
"two": ["o'clock", "o'clock", "rock around the clock"],
},
"│───────│───────────────────────│\n\
│ one │ two │\n\
│═══════│═══════════════════════│\n\
│ three │ o'clock │\n\
│───────│───────────────────────│\n\
│ four │ o'clock │\n\
│───────│───────────────────────│\n\
│ rock │ rock around the clock │\n\
│───────│───────────────────────│",
),
],
)
def test_fill_from_dict(dict_, expected):
table = tui.Table()
table.fill_from_dict(dict_)
assert table.draw() == expected
@pytest.mark.parametrize(
("dict_list", "expected"),
[
(
(
[
{"one": "three", "two": "o'clock"},
{"one": "four", "two": "o'clock"},
{"one": "rock", "two": "rock around the clock"},
]
),
"│───────│───────────────────────│\n\
│ one │ two │\n\
│═══════│═══════════════════════│\n\
│ three │ o'clock │\n\
│───────│───────────────────────│\n\
│ four │ o'clock │\n\
│───────│───────────────────────│\n\
│ rock │ rock around the clock │\n\
│───────│───────────────────────│",
),
],
)
def test_fill_from_dict_list(dict_list, expected):
table = tui.Table()
table.fill_from_dict_list(dict_list)
assert table.draw() == expected
# Copyright 2016-2025 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/>.
# Copyright 2016-2025 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 unittest.mock import patch
#
# from silkaj.wot.certification import certification_confirmation
#
# @patch('builtins.input')
# def test_certification_confirmation(mock_input):
# id_to_certify = {"pubkey": "pubkeyid to certify"}
# main_id_to_certify = {"uid": "id to certify"}
# mock_input.return_value = "yes"
#
# assert certification_confirmation(
# "certifier id",
# "certifier pubkey",
# id_to_certify,
# main_id_to_certify)
#
# mock_input.assert_called_once()
#
#
# @patch('builtins.input')
# def test_certification_confirmation_no(mock_input):
# id_to_certify = {"pubkey": "pubkeyid to certify"}
# main_id_to_certify = {"uid": "id to certify"}
# mock_input.return_value = "no"
#
# assert certification_confirmation(
# "certifier id",
# "certifier pubkey",
# id_to_certify,
# main_id_to_certify) is None
#
# mock_input.assert_called_once()
# Copyright 2016-2025 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 pytest
pytest.importorskip(
"pydiscourse",
reason="pydiscourse can't be imported, skipping deathreaper tests",
)
from silkaj.wot import exclusions
@pytest.mark.parametrize(
(
"api_id",
"duniter_forum_api_key",
"ml_forum_api_key",
"publish",
"currency",
"error",
),
[
(None, None, None, False, "g1", False),
(None, None, None, True, "g1", True),
("DeathReaper", None, None, True, "g1", True),
(None, "key", None, True, "g1", True),
(None, None, "key", True, "g1", True),
("DeathReaper", "key", "key", True, "g1", False),
("DeathReaper", "key", None, True, "g1-test", False),
],
)
def test_check_options(
api_id,
duniter_forum_api_key,
ml_forum_api_key,
publish,
currency,
error,
capsys,
):
if error:
msg = "Error: To be able to publish, api_id"
with pytest.raises(SystemExit, match=msg):
exclusions.check_options(
api_id,
duniter_forum_api_key,
ml_forum_api_key,
publish,
currency,
)
else:
exclusions.check_options(
api_id,
duniter_forum_api_key,
ml_forum_api_key,
publish,
currency,
)
assert not capsys.readouterr().out
# def test_generate_identity_info():
# ref = "- **Certifié·e par** @GildasMalassinet, @Marcolibri, @Gregory et @gpsqueeek.\n\
# - **A certifié** @lesailesbleues, @Marcolibri, @Gregory et @LaureBLodjtahne.\n\
# - **Exclu·e le** 2020-01-27 04:06:09\n\
# - **Raison de l'exclusion** : manque de certifications"
# # missing lookup and block
# # 292110
# raw_block = "Version: 11\nType: Block\nCurrency: g1\nNumber: 292110\nPoWMin: 89\n\
# Time: 1580098477\nMedianTime: 1580094369\nUnitBase: 0\n\
# Issuer: D3krfq6J9AmfpKnS3gQVYoy7NzGCc61vokteTS8LJ4YH\nIssuersFrame: 161\n\
# IssuersFrameVar: 0\nDifferentIssuersCount: 32\n\
# PreviousHash: 0000009F88010580F54C0403BC87BF10E912D0FBC274294343DDC748F697991D\n\
# PreviousIssuer: 5P9HB1uBdE3ZqThSatBdBbjVmh3psa1BhsCRBumn1aMo\nMembersCount: 2527\n\
# Identities:\nJoiners:\nActives:\nLeavers:\nRevoked:\nExcluded:\n\
# DKWXiEEd4wkf4pgd95vkGnNkHxzTKDpPPSgMay5WJ7Fc\nCertifications:\nTransactions:\n\
# InnerHash: CD17661EBD858CC374918D2F94363E22AB5BDC617FDEEA729C384616FD2285C4\n\
# Nonce: 10800000223569\n"
# block_sig = "GvREqrzvbS2RHoLCEbfjjPcsJ+G+jVKqejF5fu9i3076XJ4pmc+udyCPoJ5TiCo7OPB5GOLmsCLocJZbqQS1CA=="
# block = Block.from_signed_raw(raw_block + block_sig + "\n")
# gen = exclusions.generate_identity_info(lookup, block, 5)
# assert gen == ref
def test_elements_inbetween_list():
uids = ["toto", "titi", "tata"]
assert exclusions.elements_inbetween_list(0, uids) == " "
assert exclusions.elements_inbetween_list(1, uids) == ", "
assert exclusions.elements_inbetween_list(2, uids) == " et "
@pytest.mark.parametrize(
("message", "publish", "currency", "forum"),
[("message", False, "g1", "duniter")],
)
def test_publish_display(message, publish, currency, forum, capsys):
exclusions.publish_display(None, None, message, publish, currency, forum)
captured = capsys.readouterr()
if not publish:
assert captured.out == "message\n"
@pytest.mark.parametrize(
("currency", "forum"),
[("g1", "duniter"), ("g1", "monnaielibre"), ("g1-test", "duniter")],
)
def test_get_topic_id(currency, forum):
expected_topic_id = exclusions.get_topic_id(currency, forum)
if currency == "g1":
if forum == "duniter":
topic_id = exclusions.DUNITER_FORUM_G1_TOPIC_ID
else:
topic_id = exclusions.MONNAIE_LIBRE_FORUM_G1_TOPIC_ID
else:
topic_id = exclusions.DUNITER_FORUM_GTEST_TOPIC_ID
assert topic_id == expected_topic_id
@pytest.mark.parametrize(
("forum", "response", "topic_id"),
[
("duniter", {"topic_slug": "topic-slug"}, exclusions.DUNITER_FORUM_G1_TOPIC_ID),
(
"monnaielibre",
{"topic_slug": "silkaj"},
exclusions.MONNAIE_LIBRE_FORUM_G1_TOPIC_ID,
),
(
"duniter",
{"topic_slug": "how-to-monnaie-libre"},
exclusions.DUNITER_FORUM_G1_TOPIC_ID,
),
],
)
def test_publication_link(forum, response, topic_id, capsys):
if forum == "duniter":
forum_url = exclusions.DUNITER_FORUM_URL
else:
forum_url = exclusions.MONNAIE_LIBRE_FORUM_URL
expected = "Published on {}t/{}/{}/last\n".format(
forum_url,
response["topic_slug"],
str(topic_id),
)
exclusions.publication_link(forum, response, topic_id)
captured = capsys.readouterr()
assert expected == captured.out
# Copyright 2016-2025 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 re
import urllib
import pendulum
import pytest
from duniterpy.api import bma
from duniterpy.documents.block_id import BlockID
from duniterpy.documents.identity import Identity
from silkaj.constants import ALL, PUBKEY_PATTERN
from silkaj.public_key import gen_pubkey_checksum
from silkaj.wot import idty_tools
from tests.patched.idty_tools import (
idty1,
idty2,
idty_block,
lookup_one,
lookup_three,
lookup_two,
)
# used test identities
id_moul_test_1 = Identity(
currency="g1-test",
pubkey="5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH",
uid="moul-test",
block_id=BlockID.from_str(
"167750-0000A51FF952B76AAA594A46CA0C8156A56988D2B2B57BE18ECB4F3CFC25CEC2",
),
)
id_moul_test_1.signature = "/15YBc4JDPvKD4c8nWD6C0XN0krrS32uDRSH6rJvM\
Fih/H5nPc8oiCgL27bA7P3NPnp+oCqbS12QygQRnhoDDQ=="
id_elois_test = Identity(
currency="g1-test",
pubkey="D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C",
uid="elois",
block_id=BlockID.from_str(
"0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
),
)
id_elois_test.signature = "HFmZy01XSXSSsqRWYKR+nIsaReBruJUqYHqJh3EtdR\
kHV3i30S2lM32pM2w4GHYXNIiOf4+NXzcmuEQVkewBAA=="
id_moul_test_3 = Identity(
version=10,
currency="g1-test",
pubkey="2M3Et298TDYXDcsixp82iEc4ijzuiyQtsiQac2QjCRgA",
uid="moul-test",
block_id=BlockID.from_str(
"287573-00003674E9A2B5127327542CFA36BCB95D05E8EBD8AAF9C684B19EB7502161D4",
),
)
id_moul_test_3.signature = "BB1Ete898yN/ZwQn4H7o0gsS1JD05zZBSd7qdU2Am\
SSZLtjG199fN0Z5jKjQi7S2IVvrH0G5cft74sufVS3+Cw=="
# idty lists for following tests
ids_list_for_merge_lists = [
id_moul_test_1,
id_elois_test,
id_moul_test_3,
]
ids_list_for_lookups_test = [
id_moul_test_1,
id_elois_test,
]
# invalid identity
id_claudius_maximus = Identity(
currency="g1-test",
pubkey="6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o",
uid="Claudius Maximus",
block_id=BlockID.from_str(
"7334-002A45E751DCA7535D4F0A082F493E2C8EFF07612683525EB5DA92B6D17C30BD",
),
)
id_claudius_maximus.signature = (
"wrong_sig_x4PZODx0Wf+xdXAJTmYD+yqdyZBsPF7UwqdaCA4N+yHj7+09Gjsttl0i9GtWzodyJ6mBE1q7jcAw==",
)
def test_display_alternate_ids():
"""
Tests the display of identities table.
When using pytest `-s` option, this test only works on terminal default width of 80.
"""
ids_list = [
id_moul_test_1,
id_elois_test,
id_claudius_maximus,
]
expected = (
"+------------------+--------------------------------------------+--------------+\n\
| uid | public key | timestamp |\n\
+==================+============================================+==============+\n\
| moul-test | 5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1X | 167750-0000A |\n\
| | YH:baK | |\n\
+------------------+--------------------------------------------+--------------+\n\
| elois | D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3 | 0-E3B0C44298 |\n\
| | C:6Ns | |\n\
+------------------+--------------------------------------------+--------------+\n\
| Claudius Maximus | 6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj | 7334-002A45E |\n\
| | 7o:7xo | |\n\
+------------------+--------------------------------------------+--------------+"
)
table = idty_tools.display_alternate_ids(ids_list)
assert expected == table.draw()
@pytest.mark.parametrize(
("lookups_pubkey", "lookups_uid", "currency", "expected"),
[
(
[
{
"pubkey": "5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH",
"uids": [
{
"uid": "moul-test",
"meta": {
"timestamp": "167750-0000A51FF952B76A\
AA594A46CA0C8156A56988D2B2B57BE18ECB4F3CFC25CEC2",
},
"revoked": False,
"revoked_on": None,
"revocation_sig": None,
"self": "/15YBc4JDPvKD4c8nWD6C0XN0krrS32u\
DRSH6rJvMFih/H5nPc8oiCgL27bA7P3NPnp+oCqbS12QygQRnhoDDQ==",
"others": [],
},
],
"signed": [],
},
{
"pubkey": "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C",
"uids": [
{
"uid": "elois",
"meta": {
"timestamp": "0-E3B0C44298FC1C149AFBF\
4C8996FB92427AE41E4649B934CA495991B7852B855",
},
"revoked": True,
"revoked_on": 1540589179,
"revocation_sig": None,
"self": "HFmZy01XSXSSsqRWYKR+nIsaReBruJUq\
YHqJh3EtdRkHV3i30S2lM32pM2w4GHYXNIiOf4+NXzcmuEQVkewBAA==",
"others": [],
},
],
"signed": [],
},
],
[
{
"pubkey": "5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH",
"uids": [
{
"uid": "moul-test",
"meta": {
"timestamp": "167750-0000A51FF952B76A\
AA594A46CA0C8156A56988D2B2B57BE18ECB4F3CFC25CEC2",
},
"revoked": False,
"revoked_on": None,
"revocation_sig": None,
"self": "/15YBc4JDPvKD4c8nWD6C0XN0krrS32u\
DRSH6rJvMFih/H5nPc8oiCgL27bA7P3NPnp+oCqbS12QygQRnhoDDQ==",
"others": [],
},
],
"signed": [],
},
{
"pubkey": "2M3Et298TDYXDcsixp82iEc4ijzuiyQtsiQac2QjCRgA",
"uids": [
{
"uid": "moul-test",
"meta": {
"timestamp": "287573-00003674E9A2B512\
7327542CFA36BCB95D05E8EBD8AAF9C684B19EB7502161D4",
},
"revoked": True,
"revoked_on": 1544923049,
"revocation_sig": "oRBunChfKUUMqZLCB+0QO8\
LpQcpx9FZJNXIt79Q/zRPpi9X1hNUPKV4myMxHBSVI6YbPB+gBw/Bb+n3kaIuRAg==",
"self": "BB1Ete898yN/ZwQn4H7o0gsS1JD05zZB\
Sd7qdU2AmSSZLtjG199fN0Z5jKjQi7S2IVvrH0G5cft74sufVS3+Cw==",
"others": [],
},
],
"signed": [],
},
],
"g1-test",
ids_list_for_merge_lists,
),
],
)
def test_merge_ids_lists(lookups_pubkey, lookups_uid, currency, expected):
for element, expect in zip(
idty_tools.merge_ids_lists(lookups_pubkey, lookups_uid, currency),
expected,
):
assert element.signed_raw() == expect.signed_raw()
@pytest.mark.parametrize(
("lookups", "currency", "expected"),
[
(
[
{
"pubkey": "5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH",
"uids": [
{
"uid": "moul-test",
"meta": {
"timestamp": "167750-0000A51FF952B76A\
AA594A46CA0C8156A56988D2B2B57BE18ECB4F3CFC25CEC2",
},
"revoked": False,
"revoked_on": None,
"revocation_sig": None,
"self": "/15YBc4JDPvKD4c8nWD6C0XN0krrS32u\
DRSH6rJvMFih/H5nPc8oiCgL27bA7P3NPnp+oCqbS12QygQRnhoDDQ==",
"others": [],
},
],
"signed": [],
},
{
"pubkey": "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C",
"uids": [
{
"uid": "elois",
"meta": {
"timestamp": "0-E3B0C44298FC1C149AFBF\
4C8996FB92427AE41E4649B934CA495991B7852B855",
},
"revoked": True,
"revoked_on": 1540589179,
"revocation_sig": None,
"self": "HFmZy01XSXSSsqRWYKR+nIsaReBruJUq\
YHqJh3EtdRkHV3i30S2lM32pM2w4GHYXNIiOf4+NXzcmuEQVkewBAA==",
"others": [],
},
],
"signed": [],
},
],
"g1-test",
ids_list_for_lookups_test,
),
],
)
def test_ids_list_from_lookups(lookups, currency, expected):
for element, expect in zip(
idty_tools.ids_list_from_lookups(lookups, currency),
expected,
):
assert element.signed_raw() == expect.signed_raw()
# test display_identity
@pytest.mark.parametrize("idty", [(idty1)])
def test_display_identity(idty, monkeypatch, capsys):
def patch_get_id_block(node, number):
return idty_block
monkeypatch.setattr(bma.blockchain, "block", patch_get_id_block)
idty_table = idty_tools.display_identity(idty)
result = idty_table.draw()
assert "| Public key | 6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o:7xo" in result
assert "| User ID | Claude" in result
assert "| Blockstamp | 597334" in result
# idty_block returns a block at timestamp 1594980185,.
creation_time = pendulum.from_timestamp(idty_block["time"], tz="local").format(ALL)
assert creation_time in result
@pytest.mark.parametrize(
("idty", "lookup_pk", "lookup_uid", "expected", "expect_bool"),
[
# normal case : 1 same lookup on pubkey and uid
(
idty1,
lookup_one,
lookup_one,
None,
True,
),
# 2 lookups on pubkey, one on uid, match similar.
(
idty1,
lookup_one,
lookup_two,
[
"One matching identity!",
"Similar identities:",
"Claudia",
],
True,
),
# 2 lookups on pubkey, one on uid, no match similar.
(
idty2,
lookup_one,
lookup_two,
[
"Identity document does not match any valid identity.",
"Claude",
"Claudia",
],
False,
),
# 1 lookup on pubkey, 2 on uid, match.
(
idty1,
lookup_one,
lookup_three,
[
"One matching identity!",
"Similar identities:",
"Claude",
"XXXX",
],
True,
),
# 1 lookup on pubkey, 2 on uid, no match.
(
idty2,
lookup_one,
lookup_three,
[
"Identity document does not match any valid identity.",
"Similar identities:",
"Claude",
"XXXX",
],
False,
),
# no lookup found.
(
idty1,
None,
None,
[
f"Identity document does not match any valid identity.\n\
uid: {idty1.uid}\npubkey: {gen_pubkey_checksum(idty1.pubkey)}",
],
False,
),
],
)
def test_check_many_identities(
idty,
lookup_pk,
lookup_uid,
expected,
expect_bool,
monkeypatch,
capsys,
):
# Patch BMA lookup so it returns different lookups if we request a pk or a uid
def patched_bma_lookup(whatever_node, lookup):
# raise errors if no matching identity
http_error = urllib.error.HTTPError(
url="this.is/a/test/url",
code=404,
msg="Not Found TEST",
hdrs={},
fp=None,
)
if re.search(PUBKEY_PATTERN, lookup):
if not lookup_pk:
raise http_error
return lookup_pk
if not lookup_uid:
raise http_error
return lookup_uid
monkeypatch.setattr(bma.wot, "lookup", patched_bma_lookup)
# identity does not exist
if expected == [
f"Identity document does not match any valid identity.\
\nuid: {idty.uid}\npubkey: {gen_pubkey_checksum(idty.pubkey)}",
]:
with pytest.raises(SystemExit) as pytest_exit:
result = idty_tools.check_many_identities(idty)
assert pytest_exit.type == SystemExit
assert expected[0] == str(pytest_exit.value.code)
# test cases with an identity
else:
result = idty_tools.check_many_identities(idty)
assert result == expect_bool
display_result = capsys.readouterr().out
if expected:
for expect in expected:
assert expect in display_result
# Copyright 2016-2025 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 pendulum
import pytest
from duniterpy.api import bma
from duniterpy.documents import Membership, get_block_id
from duniterpy.key import SigningKey
from silkaj import tui
from silkaj.blockchain import tools as bc_tools
from silkaj.constants import DATE
from silkaj.network import client_instance
from silkaj.public_key import gen_pubkey_checksum
from silkaj.wot import membership
from tests.patched.blockchain_tools import (
currency,
fake_block_id,
patched_block,
patched_params,
)
from tests.patched.wot import (
patched_wot_requirements_no_pending,
patched_wot_requirements_one_pending,
)
# Values and patches
PUBKEY = "EA7Dsw39ShZg4SpURsrgMaMqrweJPUFPYHwZA8e92e3D"
identity_block_id = get_block_id(
"0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
)
identity_uid = "toto"
membership_block_id = fake_block_id
def patched_auth_method():
return SigningKey.from_credentials(identity_uid, identity_uid)
@pytest.mark.parametrize(
"patched_wot_requirements",
[patched_wot_requirements_no_pending, patched_wot_requirements_one_pending],
)
def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsys):
monkeypatch.setattr(bma.wot, "requirements", patched_wot_requirements)
monkeypatch.setattr(bma.blockchain, "parameters", patched_params)
monkeypatch.setattr(bma.blockchain, "block", patched_block)
client = client_instance()
identities_requirements = client(bma.wot.requirements, search=PUBKEY, pubkey=True)
for identity_requirements in identities_requirements["identities"]:
if identity_requirements["uid"] == identity_uid:
membership_expires = identity_requirements["membershipExpiresIn"]
pending_expires = identity_requirements["membershipPendingExpiresIn"]
pending_memberships = identity_requirements["pendingMemberships"]
break
table = []
if membership_expires:
table.append(
[
"Expiration date of current membership",
pendulum.now().add(seconds=membership_expires).diff_for_humans(),
],
)
if pending_memberships:
table.append(
[
"Number of pending membership(s) in the mempool",
len(pending_memberships),
],
)
table.append(
[
"Pending membership documents will expire",
pendulum.now().add(seconds=pending_expires).diff_for_humans(),
],
)
table.append(["User Identifier (UID)", identity_uid])
table.append(["Public Key", gen_pubkey_checksum(PUBKEY)])
table.append(["Block Identity", str(identity_block_id)[:45] + ""])
block = client(bma.blockchain.block, identity_block_id.number)
table.append(
[
"Identity published",
pendulum.from_timestamp(block["time"], tz="local").format(DATE),
],
)
params = bc_tools.get_blockchain_parameters()
table.append(
[
"Expiration date of new membership",
pendulum.now().add(seconds=params["msValidity"]).diff_for_humans(),
],
)
table.append(
[
"Expiration date of new membership from the mempool",
pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans(),
],
)
display_table = tui.Table()
display_table.fill_rows(table)
expected = display_table.draw() + "\n"
membership.display_confirmation_table(identity_uid, PUBKEY, identity_block_id)
captured = capsys.readouterr()
assert expected == captured.out
def test_generate_membership_document():
signing_key = patched_auth_method()
generated_membership = membership.generate_membership_document(
PUBKEY,
membership_block_id,
identity_uid,
identity_block_id,
currency=currency,
key=signing_key,
)
expected = Membership(
issuer=PUBKEY,
membership_block_id=membership_block_id,
uid=identity_uid,
identity_block_id=identity_block_id,
signing_key=signing_key,
currency=currency,
)
assert expected == generated_membership
# Copyright 2016-2025 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.account_storage import AccountStorage
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_currency_gtest,
patched_get_head_block_gtest,
)
from tests.patched.idty_tools import idty1, idty2, idty_block, lookup_one, lookup_two
REVOCATION_PATH = Path("revocation.txt")
# 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,
)
def patched_account_storage_revocation_path(self):
return REVOCATION_PATH
# tests
# test cli dry-run
@pytest.mark.parametrize(
("subcommand"),
[
("publish"),
("revoke"),
],
)
def test_revocation_cli_dry_run(subcommand, monkeypatch):
"""
Tests dry-run option behavior when associated with other options
"""
monkeypatch.setattr(auth, "auth_method", patched_auth_method_Claude)
monkeypatch.setattr(
AccountStorage,
"revocation_path",
patched_account_storage_revocation_path,
)
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,
)
monkeypatch.setattr(Path, "mkdir", Mock())
warning = "WARNING: the document will only be displayed and will not be sent."
command = ("--dry-run", "-gt", "--account", "test", "wot", "revocation", subcommand)
print("command: ", " ".join(command)) # debug
runner = CliRunner()
with runner.isolated_filesystem():
REVOCATION_PATH.write_text(REV_DOC.signed_raw(), encoding="utf-8")
result = runner.invoke(cli, args=command)
assert idty1.pubkey in result.output
assert "Version: 10" in result.output
assert warning in result.output
CREATE_STRINGS = [
idty1.pubkey,
"Do you want to save the revocation document for this identity?",
]
# test cli create
@pytest.mark.parametrize(
("user_input", "expected"),
[
(
"yes\n",
CREATE_STRINGS,
),
(
None,
CREATE_STRINGS,
),
(
"no\n",
[*CREATE_STRINGS, "Ok, goodbye!"],
),
],
)
def test_revocation_cli_create(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,
)
monkeypatch.setattr(Path, "mkdir", Mock())
command = ("--account", "test", "wot", "revocation", "create")
result = CliRunner().invoke(cli, args=command, input=user_input)
if user_input == "yes\n":
patched_save_doc.assert_called_once()
elif user_input == "no\n":
patched_save_doc.assert_not_called()
for expect in expected:
assert expect in result.output
# test cli verify
@pytest.mark.parametrize(
("doc", "lookup", "expected", "not_expected"),
[
(
REV_DOC,
lookup_one,
[
"| Public key |",
"Revocation document is valid.\n",
],
[],
),
(
REV_DOC,
lookup_two,
[
"One matching identity!\nSimilar identities:",
"uid",
"| Public key |",
"Revocation document is valid.\n",
],
[],
),
(
REV_DOC_FALSE,
lookup_one,
["Error: the signature of the revocation document is invalid."],
[
"| Public key |",
"Revocation document is valid.\n",
],
),
(
REV_2,
lookup_two,
[
"Revocation document does not match any valid identity.\nSimilar identities:",
"uid",
"Claude",
"Claudia",
],
[
"Revocation document is valid.\n",
"| Public key |",
],
),
],
)
def test_revocation_cli_verify(
doc,
lookup,
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)
monkeypatch.setattr(
AccountStorage,
"revocation_path",
patched_account_storage_revocation_path,
)
monkeypatch.setattr(Path, "mkdir", Mock())
command = ("--account", "test", "wot", "revocation", "verify")
runner = CliRunner()
with runner.isolated_filesystem():
REVOCATION_PATH.write_text(doc.signed_raw(), encoding="utf-8")
result = CliRunner().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", "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?",
],
),
(
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.",
],
),
(
True,
False,
REV_DOC,
lookup_one,
"yes\n",
[
"| Public key |",
"Do you confirm sending this revocation document immediately?",
],
),
(
False,
True,
REV_DOC,
lookup_one,
"yes\n",
[
"WARNING: the document will only be displayed and will not be sent.",
"Version: 10",
],
),
(
False,
True,
REV_DOC_FALSE,
lookup_one,
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,
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)
monkeypatch.setattr(
AccountStorage,
"revocation_path",
patched_account_storage_revocation_path,
)
patched_send_bma_revoke = Mock()
monkeypatch.setattr(bma.wot, "revoke", patched_send_bma_revoke)
monkeypatch.setattr(Path, "mkdir", Mock())
# prepare command
command = display_dry_options(display, dry_run)
command.extend(["--account", "test", "wot", "revocation", "publish"])
# test publication
runner = CliRunner()
with runner.isolated_filesystem():
REVOCATION_PATH.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", "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",
],
),
],
)
def test_revocation_cli_publish_send_errors(
display,
user_input,
expected,
monkeypatch,
):
def patched_lookup(node, id_pubkey):
return lookup_one
monkeypatch.setattr(
AccountStorage,
"revocation_path",
patched_account_storage_revocation_path,
)
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)
monkeypatch.setattr(Path, "mkdir", Mock())
# prepare command
command = display_dry_options(display, False)
command.extend(["--account", "test", "wot", "revocation", "publish"])
# test publication
runner = CliRunner()
with runner.isolated_filesystem():
REVOCATION_PATH.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,
[
idty1.pubkey,
"Do you confirm sending this revocation document immediately?",
],
),
(
True,
False,
"yes\n",
REV_DOC,
[
idty1.pubkey,
"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.",
idty1.pubkey,
"Version: 10",
],
),
(
False,
False,
"no\n",
REV_DOC,
[
idty1.pubkey,
"Do you confirm sending this revocation document immediately?",
],
),
(
True,
False,
"no\n",
REV_DOC,
[
idty1.pubkey,
"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_currency", patched_get_currency_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,
[
idty1.pubkey,
"Do you confirm sending this revocation document immediately?",
"Error while publishing revocation",
],
),
(
True,
"yes\n",
REV_DOC,
[
idty1.pubkey,
"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],
idty1.pubkey,
"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"),
[
(
Path("./test_doc.txt"),
REV_DOC,
REV_2,
idty1.pubkey,
),
(
Path("revocation"),
REV_DOC,
REV_2,
idty1.pubkey,
),
],
)
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
revocation.save_doc(path, rev_1.signed_raw(), pubkey)
assert path.is_file()
assert path.read_text(encoding="utf-8") == rev_1.signed_raw()
# test file has 600 permission
assert oct(path.stat().st_mode)[-3:] == "600"
# 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.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.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 = Path("test_file.txt")
monkeypatch.setattr(bma.wot, "lookup", patched_lookup)
# test
runner = CliRunner()
with runner.isolated_filesystem():
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 = Path("test_file.txt")
monkeypatch.setattr(bma.wot, "lookup", patched_lookup)
# test
runner = CliRunner()
with runner.isolated_filesystem():
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
file = Path("test_file.txt")
patched_lookup = Mock()
monkeypatch.setattr(bma.wot, "lookup", patched_lookup)
# test
runner = CliRunner()
with runner.isolated_filesystem():
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(file)
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()
# Copyright 2016-2025 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 pytest
import rich_click as click
from silkaj.wot import tools as w_tools
pubkey_titi_tata = "B4RoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88"
pubkey_toto_tutu = "totoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88"
pubkey_titi_tata_checksum = "B4RoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88:9iP"
def identity_card(uid, timestamp):
return {
"uid": uid,
"meta": {"timestamp": timestamp},
}
titi = identity_card(
"titi",
"590358-000156C5620946D1D63DAF82BF3AA735CE0B3518D59274171C88A7DBA4C906BC",
)
tata = identity_card(
"tata",
"210842-000000E7AAC79A07F487B33A48B3217F8A1F0A31CDB42C5DFC5220A20665B6B1",
)
toto = identity_card(
"toto",
"189601-0000011405B5C96EA69C1273370E956ED7887FA56A75E3EFDF81E866A2C49FD9",
)
tutu = identity_card(
"tutu",
"389601-0000023405B5C96EA69C1273370E956ED7887FA56A75E3EFDF81E866A2C49FD9",
)
def patched_lookup_one(pubkey_uid):
return [
{
"pubkey": pubkey_titi_tata,
"uids": [titi],
"signed": [],
},
]
def patched_lookup_two(pubkey_uid):
return [
{
"pubkey": pubkey_titi_tata,
"uids": [titi, tata],
"signed": [],
},
]
def patched_lookup_three(pubkey_uid):
return [
{
"pubkey": pubkey_titi_tata,
"uids": [titi, tata],
"signed": [],
},
{
"pubkey": pubkey_toto_tutu,
"uids": [toto],
"signed": [],
},
]
def patched_lookup_four(pubkey_uid):
return [
{
"pubkey": pubkey_titi_tata,
"uids": [titi, tata],
"signed": [],
},
{
"pubkey": pubkey_toto_tutu,
"uids": [toto, tutu],
"signed": [],
},
]
def patched_lookup_five(pubkey_uid):
return [
{
"pubkey": pubkey_titi_tata,
"uids": [titi],
"signed": [],
},
{
"pubkey": pubkey_toto_tutu,
"uids": [titi],
"signed": [],
},
]
def patched_prompt_titi(message):
return "00"
def patched_prompt_tata(message):
return "01"
def patched_prompt_toto(message):
return "10"
def patched_prompt_tutu(message):
return "11"
@pytest.mark.parametrize(
("selected_uid", "pubkey", "patched_prompt", "patched_lookup"),
[
("titi", pubkey_titi_tata, patched_prompt_titi, patched_lookup_one),
("titi", pubkey_titi_tata_checksum, patched_prompt_titi, patched_lookup_one),
("tata", pubkey_titi_tata, patched_prompt_tata, patched_lookup_two),
("toto", pubkey_toto_tutu, patched_prompt_toto, patched_lookup_three),
("tutu", pubkey_toto_tutu, patched_prompt_tutu, patched_lookup_four),
("titi", pubkey_toto_tutu, patched_prompt_toto, patched_lookup_five),
],
)
def test_choose_identity(
selected_uid,
pubkey,
patched_prompt,
patched_lookup,
capsys,
monkeypatch,
):
monkeypatch.setattr(w_tools, "wot_lookup", patched_lookup)
monkeypatch.setattr(click, "prompt", patched_prompt)
idty_card, get_pubkey, signed = w_tools.choose_identity(pubkey)
expected_pubkey = pubkey.split(":")[0]
assert expected_pubkey == get_pubkey
assert selected_uid == idty_card["uid"]
# Check whether the table is not displayed in case of one identity
# Check it is displayed for more than one identity
# Check the uids and ids are in
captured = capsys.readouterr()
lookups = patched_lookup("")
# only one pubkey and one uid on this pubkey
if len(lookups) == 1 and len(lookups[0]["uids"]) == 1:
assert not captured.out
# many pubkeys or many uid on one pubkey
else:
# if more than one pubkey, there should be a "10" numbering
if len(lookups) > 1:
assert " 10 " in captured.out
for lookup in lookups:
if len(lookup["uids"]) > 1:
assert " 01 " in captured.out
for uid in lookup["uids"]:
assert uid["uid"] in captured.out