2023-11-13 12:34:43 +00:00
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
#
|
|
|
|
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
|
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
|
|
# copies or substantial portions of the Software.
|
|
|
|
#
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
# SOFTWARE.
|
|
|
|
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Tests for the feed functionality"""
|
|
|
|
|
2017-05-25 10:47:01 +00:00
|
|
|
from base64 import b64decode
|
|
|
|
from collections import OrderedDict
|
2023-11-14 17:47:17 +00:00
|
|
|
from datetime import datetime, timezone
|
2017-05-25 10:47:01 +00:00
|
|
|
|
2017-08-05 17:32:01 +00:00
|
|
|
from nacl.signing import SigningKey, VerifyKey
|
2023-11-14 04:00:03 +00:00
|
|
|
import pytest
|
2023-11-14 17:47:17 +00:00
|
|
|
from pytest_mock import MockerFixture
|
2017-05-25 10:47:01 +00:00
|
|
|
|
2023-11-14 04:00:03 +00:00
|
|
|
from ssb.feed import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
|
2023-11-14 17:47:17 +00:00
|
|
|
from ssb.feed.models import get_millis_1970
|
2017-08-06 10:16:22 +00:00
|
|
|
|
|
|
|
SERIALIZED_M1 = b"""{
|
|
|
|
"previous": null,
|
|
|
|
"author": "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519",
|
|
|
|
"sequence": 1,
|
|
|
|
"timestamp": 1495706260190,
|
|
|
|
"hash": "sha256",
|
|
|
|
"content": {
|
|
|
|
"type": "about",
|
|
|
|
"about": "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519",
|
|
|
|
"name": "neo",
|
|
|
|
"description": "The Chosen One"
|
|
|
|
},
|
|
|
|
"signature": "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
|
|
|
|
}"""
|
2017-05-25 10:47:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2023-11-01 06:22:29 +00:00
|
|
|
def local_feed() -> LocalFeed:
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Fixture providing a local feed"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
|
2017-08-05 16:21:14 +00:00
|
|
|
return LocalFeed(SigningKey(secret))
|
2017-05-25 10:47:01 +00:00
|
|
|
|
|
|
|
|
2017-08-05 17:32:01 +00:00
|
|
|
@pytest.fixture()
|
2023-11-01 06:22:29 +00:00
|
|
|
def remote_feed() -> Feed:
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Fixture providing a remote feed"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
|
2017-08-05 17:32:01 +00:00
|
|
|
return Feed(VerifyKey(public))
|
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_local_feed() -> None:
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test a local feed"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
|
2017-08-05 16:21:14 +00:00
|
|
|
feed = LocalFeed(SigningKey(secret))
|
|
|
|
assert bytes(feed.private_key) == secret
|
2023-11-01 04:04:43 +00:00
|
|
|
assert bytes(feed.public_key) == b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
|
|
|
|
assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
|
2017-05-25 10:47:01 +00:00
|
|
|
|
|
|
|
|
2023-11-13 12:14:25 +00:00
|
|
|
def test_local_feed_set_pubkey(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
|
|
|
|
"""Test setting only the public key for a local feed"""
|
|
|
|
|
|
|
|
key = SigningKey.generate().verify_key
|
|
|
|
|
|
|
|
with pytest.raises(TypeError) as ctx:
|
|
|
|
local_feed.public_key = key
|
|
|
|
|
|
|
|
assert str(ctx.value) == "Can not set only the public key for a local feed"
|
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_remote_feed() -> None:
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test a remote feed"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
|
2017-08-05 17:32:01 +00:00
|
|
|
feed = Feed(VerifyKey(public))
|
|
|
|
assert bytes(feed.public_key) == public
|
2023-11-01 04:04:43 +00:00
|
|
|
assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
|
|
|
|
|
|
|
|
m1 = Message(
|
|
|
|
feed,
|
2023-11-01 06:22:29 +00:00
|
|
|
OrderedDict([("type", "about"), ("about", feed.id), ("name", "neo"), ("description", "The Chosen One")]),
|
2023-11-01 04:04:43 +00:00
|
|
|
"foo",
|
|
|
|
timestamp=1495706260190,
|
|
|
|
)
|
2017-08-06 10:16:22 +00:00
|
|
|
|
|
|
|
with pytest.raises(NoPrivateKeyException):
|
2023-11-01 06:22:29 +00:00
|
|
|
feed.sign(m1.serialize())
|
2017-08-06 10:16:22 +00:00
|
|
|
|
2017-08-05 17:32:01 +00:00
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test a local message"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
m1 = LocalMessage(
|
|
|
|
local_feed,
|
2023-11-01 06:22:29 +00:00
|
|
|
OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
|
2023-11-01 04:04:43 +00:00
|
|
|
timestamp=1495706260190,
|
|
|
|
)
|
2017-05-25 10:47:01 +00:00
|
|
|
assert m1.timestamp == 1495706260190
|
|
|
|
assert m1.previous is None
|
|
|
|
assert m1.sequence == 1
|
2023-11-01 04:04:43 +00:00
|
|
|
assert (
|
|
|
|
m1.signature
|
|
|
|
== "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
|
|
|
|
)
|
|
|
|
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
|
|
|
|
|
|
|
|
m2 = LocalMessage(
|
|
|
|
local_feed,
|
|
|
|
OrderedDict(
|
|
|
|
[
|
|
|
|
("type", "about"),
|
|
|
|
("about", local_feed.id),
|
|
|
|
("name", "morpheus"),
|
|
|
|
("description", "Dude with big jaw"),
|
|
|
|
]
|
|
|
|
),
|
|
|
|
previous=m1,
|
|
|
|
timestamp=1495706447426,
|
|
|
|
)
|
2017-05-25 10:47:01 +00:00
|
|
|
assert m2.timestamp == 1495706447426
|
|
|
|
assert m2.previous is m1
|
|
|
|
assert m2.sequence == 2
|
2023-11-01 04:04:43 +00:00
|
|
|
assert (
|
|
|
|
m2.signature
|
|
|
|
== "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
|
|
|
|
)
|
|
|
|
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
|
2017-08-05 17:32:01 +00:00
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_remote_message(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test a remote message"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
signature = "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
|
|
|
|
m1 = Message(
|
|
|
|
remote_feed,
|
2023-11-01 06:22:29 +00:00
|
|
|
OrderedDict([("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
|
2023-11-01 04:04:43 +00:00
|
|
|
signature,
|
|
|
|
timestamp=1495706260190,
|
|
|
|
)
|
2017-08-05 17:32:01 +00:00
|
|
|
assert m1.timestamp == 1495706260190
|
|
|
|
assert m1.previous is None
|
|
|
|
assert m1.sequence == 1
|
|
|
|
assert m1.signature == signature
|
2023-11-01 04:04:43 +00:00
|
|
|
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
|
|
|
|
|
|
|
|
signature = "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
|
|
|
|
m2 = Message(
|
|
|
|
remote_feed,
|
|
|
|
OrderedDict(
|
2023-11-01 06:22:29 +00:00
|
|
|
[("type", "about"), ("about", remote_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
|
2023-11-01 04:04:43 +00:00
|
|
|
),
|
|
|
|
signature,
|
|
|
|
previous=m1,
|
|
|
|
timestamp=1495706447426,
|
|
|
|
)
|
2017-08-05 17:32:01 +00:00
|
|
|
assert m2.timestamp == 1495706447426
|
|
|
|
assert m2.previous is m1
|
|
|
|
assert m2.sequence == 2
|
|
|
|
assert m2.signature == signature
|
|
|
|
m2.verify(signature)
|
2023-11-01 04:04:43 +00:00
|
|
|
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
|
2017-08-05 17:32:01 +00:00
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_remote_no_signature(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test remote feed without a signature"""
|
|
|
|
|
2017-08-05 17:32:01 +00:00
|
|
|
with pytest.raises(ValueError):
|
2023-11-01 04:04:43 +00:00
|
|
|
Message(
|
|
|
|
remote_feed,
|
|
|
|
OrderedDict(
|
2023-11-01 06:22:29 +00:00
|
|
|
[("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]
|
2023-11-01 04:04:43 +00:00
|
|
|
),
|
|
|
|
None,
|
|
|
|
timestamp=1495706260190,
|
|
|
|
)
|
2017-08-05 18:01:17 +00:00
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_serialize(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test feed serialization"""
|
|
|
|
|
2023-11-01 04:04:43 +00:00
|
|
|
m1 = LocalMessage(
|
|
|
|
local_feed,
|
2023-11-01 06:22:29 +00:00
|
|
|
OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
|
2023-11-01 04:04:43 +00:00
|
|
|
timestamp=1495706260190,
|
|
|
|
)
|
2017-08-05 18:01:17 +00:00
|
|
|
|
2017-08-06 10:16:22 +00:00
|
|
|
assert m1.serialize() == SERIALIZED_M1
|
|
|
|
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
def test_parse(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
|
2023-11-01 05:03:06 +00:00
|
|
|
"""Test feed parsing"""
|
|
|
|
|
2017-08-06 10:16:22 +00:00
|
|
|
m1 = LocalMessage.parse(SERIALIZED_M1, local_feed)
|
2023-11-01 06:22:29 +00:00
|
|
|
assert m1.content == {"type": "about", "about": local_feed.id, "name": "neo", "description": "The Chosen One"}
|
2017-08-06 10:16:22 +00:00
|
|
|
assert m1.timestamp == 1495706260190
|
2023-11-14 17:47:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_local_unsigned(local_feed: LocalFeed, mocker: MockerFixture) -> None: # pylint: disable=redefined-outer-name
|
|
|
|
"""Test creating an unsigned message on a local feed"""
|
|
|
|
|
|
|
|
mocked_dt = mocker.Mock(spec=datetime)
|
|
|
|
mocked_dt.utcnow = mocker.MagicMock(return_value=datetime(2023, 3, 7, 11, 45, 54, 0, tzinfo=timezone.utc))
|
|
|
|
mocker.patch("ssb.feed.models.datetime", mocked_dt)
|
|
|
|
|
2023-11-01 06:22:29 +00:00
|
|
|
msg = LocalMessage(local_feed, OrderedDict({"test": True}))
|
2023-11-14 17:47:17 +00:00
|
|
|
|
|
|
|
assert msg.feed == local_feed
|
2023-11-01 06:22:29 +00:00
|
|
|
assert msg.content == {"test": True}
|
2023-11-14 17:47:17 +00:00
|
|
|
assert msg.sequence == 1
|
|
|
|
assert msg.previous is None
|
|
|
|
assert msg.timestamp == 1678189554000
|
|
|
|
assert msg.signature == (
|
2023-11-01 06:22:29 +00:00
|
|
|
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
|
2023-11-14 17:47:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-13 12:14:25 +00:00
|
|
|
def test_local_signed(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
|
|
|
|
"""Test creating a signed message on a local feed"""
|
|
|
|
|
|
|
|
msg = LocalMessage(
|
|
|
|
local_feed,
|
|
|
|
OrderedDict({"test": True}),
|
|
|
|
timestamp=1678189554000,
|
|
|
|
signature=(
|
|
|
|
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
assert msg.feed == local_feed
|
|
|
|
assert msg.content == {"test": True}
|
|
|
|
assert msg.sequence == 1
|
|
|
|
assert msg.previous is None
|
|
|
|
assert msg.timestamp == 1678189554000
|
|
|
|
assert msg.signature == (
|
|
|
|
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-14 17:47:17 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"timestamp,expected",
|
|
|
|
(
|
|
|
|
(datetime(2023, 3, 7, 11, 45, 54, 0, tzinfo=timezone.utc), 1678189554000),
|
|
|
|
(datetime(2013, 5, 2, 2, 3, 4, 567890, tzinfo=timezone.utc), 1367460184567),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
def test_millis(timestamp: datetime, expected: int, mocker: MockerFixture) -> None:
|
|
|
|
"""Test the get_millis_1970() function"""
|
|
|
|
|
|
|
|
mocked_dt = mocker.Mock(spec=datetime)
|
|
|
|
mocked_dt.utcnow = mocker.MagicMock(return_value=timestamp)
|
|
|
|
mocker.patch("ssb.feed.models.datetime", mocked_dt)
|
|
|
|
|
|
|
|
assert get_millis_1970() == expected
|