"""Tests for the es.4 document validator""" from datetime import datetime, timedelta from nacl.signing import SigningKey import pytest from pytest_mock.plugin import MockerFixture from earthsnake.exc import ValidationError from earthsnake.document.es4 import Es4Document from earthsnake.identity import Identity from earthsnake.path import Path from earthsnake.share import Share from .helpers import VALID_DOCUMENT @pytest.mark.parametrize( 'missing', Es4Document.CORE_SCHEMA['required'], # type: ignore ) def test_validate_missing_keys(missing: str) -> None: """Test if validation fails when a required key is missing""" raw_document = VALID_DOCUMENT.copy() del raw_document[missing] with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) required_field_names = ', '.join( repr(x) for x in Es4Document.CORE_SCHEMA['required'] # type: ignore ) assert str(ctx.value) == f"data must contain [{required_field_names}] properties" def test_validate_future_document() -> None: """Test if validation fails when the document is created far in the future""" raw_document = VALID_DOCUMENT.copy() raw_document['timestamp'] = int( (datetime.utcnow() + timedelta(minutes=20)).timestamp() * 1000000 ) with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert str(ctx.value) == 'timestamp too far in the future' def test_validate_expiry_before_creation() -> None: """Test if validation fails when the document is set to expire before it has been created""" raw_document = VALID_DOCUMENT.copy() raw_document['timestamp'] = int( (datetime.utcnow() + timedelta(minutes=5)).timestamp() * 1000000 ) raw_document['deleteAfter'] = raw_document['timestamp'] - 1 with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert str(ctx.value) == 'ephemeral doc expired before it was created' def test_validate_deleteafter_in_past() -> None: """Test if validation fails if deleteAfter is in the pas""" raw_document = VALID_DOCUMENT.copy() yesterday = datetime.utcnow() - timedelta(days=1) raw_document['deleteAfter'] = int(yesterday.timestamp() * 1000000) with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert str(ctx.value) == 'ephemeral doc has expired' def test_validate_not_writable() -> None: """Test if validation fails when the author is not allowed to write at the given path""" raw_document = VALID_DOCUMENT.copy() raw_document['path'] = '/test/~@some.' + ('a' * 52) with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert ( str(ctx.value) == f'Author {raw_document["author"]} cannot write to path {raw_document["path"]}' ) def test_validate() -> None: """Test if validation succeeds on a valid document""" document = Es4Document.from_json(VALID_DOCUMENT) assert isinstance(document, Es4Document) def test_validate_ephemeral() -> None: """Test if validation succeeds with a valid deleteAfter value""" raw_document = VALID_DOCUMENT.copy() raw_document['deleteAfter'] = 9007199254740990 raw_document['signature'] = ( 'bw7zyncx7u2wrk2jz4ucfjbbc6b5t5rm7ma42vkaaius2yczvqdbvfyfbc6mh345flsaeqizw44gncot5huaskdmk' 'npkty7mgtzbssdy' ) document = Es4Document.from_json(raw_document) assert isinstance(document, Es4Document) def test_signature_check_error(mocker: MockerFixture) -> None: """Test if validation fails if something goes wrong during signature checking""" mocker.patch( 'earthsnake.document.es4.Es4Document.generate_hash', side_effect=ValueError('test error'), ) with pytest.raises(ValidationError) as ctx: Es4Document.from_json(VALID_DOCUMENT) assert str(ctx.value) == 'Cannot check signature: test error' def test_signature_check_bad_signature() -> None: """Test if validation fails if the document’s signature doesn’t match the document itself""" raw_document = VALID_DOCUMENT.copy() raw_document['signature'] = 'b' * 104 with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert str(ctx.value) == 'signature is invalid' def test_remove_extra_fields_no_underscore() -> None: """Test if remove_extra_fields() fails on non-specced fields not starting with an underscore""" raw_document = VALID_DOCUMENT.copy() raw_document.update({'test': 'yes'}) with pytest.raises(ValidationError) as ctx: Es4Document.remove_extra_fields(raw_document) assert str(ctx.value) == 'extra document fields must have names starting with an underscore' def test_remove_extra_fields_non_object() -> None: """Test if remove_extra_fields() fails if the validated document is not a JSON object""" with pytest.raises(ValidationError) as ctx: Es4Document.remove_extra_fields([]) # type: ignore assert str(ctx.value) == 'Document is not a plain JSON object' def test_remove_extra_fields() -> None: """Test if remove_extra_fields() correctly extracts field names starting with an underscore""" raw_document = VALID_DOCUMENT.copy() raw_document.update({'_test': True}) cleaned_doc, removed = Es4Document.remove_extra_fields(raw_document) assert cleaned_doc == VALID_DOCUMENT assert removed == {'_test': True} def test_check_content_hash() -> None: """Test if validation fails if contentHash is not the hash of content""" raw_document = VALID_DOCUMENT.copy() raw_document['content'] = 'other' with pytest.raises(ValidationError) as ctx: Es4Document.from_json(raw_document) assert str(ctx.value) == 'content does not match contentHash' def test_sign_different_author(identity: Identity, es4_document: Es4Document) -> None: """Test if sign() fails if the signing identity is not the same as the document author""" with pytest.raises(ValidationError) as ctx: es4_document.sign(identity=identity) assert str(ctx.value) == 'when signing a document, keypair address must match document author' def test_sign_new_identity(es4_document: Es4Document) -> None: """Test if signing a document with a different entity also sets the author""" other_key_seed = ( b'`_\x8dm\x18\xeem\xe3\\\xeb_\x1aw)\xcd\xb7\xd8\xd9\xdd\xad\x86\x9a#wQ"F\x95\xa1\x178r' ) identity = Identity('name', sign_key=SigningKey(other_key_seed)) assert str(identity) == '@name.bu5seaewd4p7cx7ot4ue3m6wpigfa5hmowxgeophe2so72roao5wq' es4_document.author = identity es4_document.sign() assert ( es4_document.signature == 'bk76oxkhbydy3itajeeqtryyj7ej5y7hqjffae6ilf4kpyklh2w32hx3ndg6cb3zvlphj46zmuxbetk4cj2fh6' '5bhxdtzflvf7oamcbi' ) assert es4_document.author == identity @pytest.mark.id_name('test') def test_sign(identity: Identity, es4_document: Es4Document) -> None: """Test if the sign() method updates the document with a correct signature""" es4_document.sign(identity=identity) assert es4_document.signature == ( 'bughac3ooy5qfect4bxdke2zkun2wqufhpivt57vj2mzq52mhqzesjvhyywttxm7qqjyhoskmiqd2lw72qy2u766r' 'fqvnbwab4qp3gbi' ) def test_content_setter(es4_document: Es4Document) -> None: """Check if the content setter recalculates content_hash, too""" es4_document.content = 'new content' assert es4_document.content_hash != VALID_DOCUMENT['contentHash'] assert es4_document.content_hash == 'b7yzgbde66w3m67r7srsiajj765xsj5hmaz4phuhqp6mejs77syaq' def test_content_hash_getter(es4_document: Es4Document) -> None: """Test if the content_hash getter recalculates the content hash if it’s not already set""" es4_document._content = 'new content' # pylint: disable=protected-access es4_document._content_hash = None # pylint: disable=protected-access assert es4_document.content_hash == 'b7yzgbde66w3m67r7srsiajj765xsj5hmaz4phuhqp6mejs77syaq' def test_validate_unsigned_document(identity: Identity) -> None: """Test if signature validation fails if there is no signature yet""" document = Es4Document(identity, Share.from_address('+test.share'), Path('/test')) with pytest.raises(ValidationError) as ctx: document.validate_signature() assert str(ctx.value) == 'document has no signature assigned' def test_content_length(es4_document: Es4Document) -> None: """Test the content_length property""" assert es4_document.content_length == 4