def test_create_opc_client( event_loop: asyncio.AbstractEventLoop, expect_user_pass: bool, mocker: MockerFixture, url: str, with_cert_file: bool, ) -> None: mocked_asyncua_client = mocker.patch("asyncua.Client") mocked_asyncua_client.return_value.set_security = mocker.AsyncMock() config = mocker.Mock(server_url=url) if with_cert_file: config.configure_mock(cert_file="certFile", private_key_file="keyFile") else: config.configure_mock(cert_file=None, private_key_file=None) opcua_client = OPCUAClient(config, mocker.Mock(), mocker.Mock(), mocker.Mock()) created_client = event_loop.run_until_complete( opcua_client._create_opc_client()) assert mocked_asyncua_client.call_args_list == [ mocker.call(url="//opc/server.url") ] set_user = cast(Mock, created_client.set_user) set_password = cast(Mock, created_client.set_password) expected_set_user_call = [] expected_set_pw_call = [] expected_set_security_call = [] if expect_user_pass: expected_set_user_call.append(mocker.call("user")) expected_set_pw_call.append(mocker.call("pass")) if with_cert_file: expected_set_security_call.append( mocker.call(SecurityPolicyBasic256Sha256, "certFile", "keyFile")) assert set_user.call_args_list == expected_set_user_call assert set_password.call_args_list == expected_set_pw_call assert created_client.set_security.await_args_list == expected_set_security_call
async def test_run( mocker: MockerFixture, root: Path, ) -> None: """ Test ``run``. """ builder = mocker.MagicMock() builder.process_post = mocker.AsyncMock() builder.process_site = mocker.AsyncMock() mocker.patch("nefelibata.cli.init.get_config") mocker.patch("nefelibata.cli.init.get_builders", return_value={"builder": builder}) _logger = mocker.patch("nefelibata.cli.init._logger") await init.run(root) builder.setup.assert_called() _logger.info.assert_called_with("Blog created!") assert (root / CONFIG_FILENAME).exists() assert (root / "posts/first/index.mkd").exists() # error if running again with pytest.raises(IOError) as excinfo: await init.run(root) assert str( excinfo.value) == "File /path/to/blog/nefelibata.yaml already exists!"
async def test_preinit(mocker: MockerFixture) -> None: from gravel.controllers.deployment.mgr import ( DeploymentError, DeploymentMgr, InitStateEnum, ) from gravel.controllers.nodes.systemdisk import OverlayError mgr: DeploymentMgr = DeploymentMgr() mocker.patch( "gravel.controllers.nodes.systemdisk.SystemDisk.exists", new=mocker.AsyncMock(return_value=False), ) await mgr.preinit() assert mgr._preinited == True assert mgr._init_state == InitStateEnum.NONE mgr = DeploymentMgr() mocker.patch( "gravel.controllers.nodes.systemdisk.SystemDisk.exists", new=mocker.AsyncMock(return_value=True), ) mocker.patch( "gravel.controllers.nodes.systemdisk.SystemDisk.enable", new=mocker.AsyncMock(side_effect=OverlayError), ) raised = False try: await mgr.preinit() except DeploymentError: raised = True except Exception: assert False assert raised assert not mgr._preinited assert mgr._init_state == InitStateEnum.NONE mgr = DeploymentMgr() mocker.patch( "gravel.controllers.nodes.systemdisk.SystemDisk.enable", new=mocker.AsyncMock(), ) await mgr.preinit() assert mgr._preinited assert mgr._init_state == InitStateEnum.INSTALLED
async def test_create( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, get_data_contents: Callable[[str, str], str], ) -> None: async def mock_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: return 0, None, None async def mock_lvm(args: str) -> None: if "lvcreate" in args: if "systemdisk" in args: fs.create_file("/dev/mapper/aquarium-systemdisk") elif "containers" in args: fs.create_file("/dev/mapper/aquarium-containers") from gravel.controllers.inventory.inventory import Inventory from gravel.controllers.inventory.nodeinfo import NodeInfoModel from gravel.controllers.nodes.systemdisk import ( SystemDisk, UnavailableDeviceError, UnknownDeviceError, ) nodeinfo: NodeInfoModel = NodeInfoModel.parse_raw( get_data_contents(DATA_DIR, "nodeinfo_real.json") ) inventory: Inventory = gstate.inventory inventory.probe = mocker.AsyncMock() inventory._latest = nodeinfo systemdisk = SystemDisk(gstate) systemdisk.lvm = mock_lvm mocker.patch( "gravel.controllers.nodes.systemdisk.aqr_run_cmd", new=mock_call ) throws = False try: await systemdisk.create("/dev/foobar") except UnknownDeviceError: throws = True pass assert throws nodeinfo.disks[0].available = False throws = False try: await systemdisk.create("/dev/vda") except UnavailableDeviceError: throws = True pass assert throws nodeinfo.disks[0].available = True await systemdisk.create("/dev/vda")
async def test_run_typecheck_in_sandbox(mocker: MockerFixture) -> None: semaphore = asyncio.Semaphore(1) mock_sandbox = mocker.AsyncMock() await run_typecheck_in_sandbox(mock_sandbox, "import this", semaphore=semaphore, python_version="3.8") mock_sandbox.run_typecheck.assert_called_once_with("import this", python_version="3.8")
def test_run(mocker: MockerFixture) -> None: """ Test ``run``. """ main = mocker.AsyncMock() mocker.patch("nefelibata.console.main", main) console.run() main.assert_called()
async def test_main_build(mocker: MockerFixture) -> None: """ Test ``main`` with the "build" action. """ build = mocker.patch("nefelibata.console.build") build.run = mocker.AsyncMock() mocker.patch( "nefelibata.console.docopt", return_value={ "--loglevel": "debug", "init": False, "new": False, "build": True, "publish": False, "ROOT_DIR": "/path/to/blog", "--force": False, }, ) await console.main() build.run.assert_called_with(Path("/path/to/blog"), False) mocker.patch( "nefelibata.console.docopt", return_value={ "--loglevel": "debug", "init": False, "new": False, "build": True, "publish": False, "ROOT_DIR": "/path/to/blog", "--force": True, }, ) await console.main() build.run.assert_called_with(Path("/path/to/blog"), True) mocker.patch( "nefelibata.console.docopt", return_value={ "--loglevel": "debug", "init": False, "new": False, "build": True, "publish": False, "ROOT_DIR": None, "--force": True, }, ) mocker.patch( "nefelibata.console.find_directory", return_value=Path("/path/to/blog"), ) await console.main() build.run.assert_called_with(Path("/path/to/blog"), True)
async def test_mgr_start( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, ) -> None: from gravel.controllers.nodes.deployment import NodeStageEnum from gravel.controllers.nodes.mgr import NodeError, NodeStateModel nodemgr = NodeMgr(gstate) assert nodemgr._state assert nodemgr.deployment_state.can_start() orig = nodemgr.deployment_state.can_start nodemgr.deployment_state.can_start = mocker.MagicMock(return_value=False) throws = False try: await nodemgr.start() except NodeError as e: assert "unstartable" in e.message throws = True assert throws nodemgr.deployment_state.can_start = orig nodemgr._deployment._state._stage = NodeStageEnum.NONE nodemgr._node_prepare = mocker.AsyncMock() await nodemgr.start() nodemgr._node_prepare.assert_called_once() # type: ignore nodemgr._deployment._state._stage = NodeStageEnum.READY nodemgr._state = NodeStateModel( uuid="bba35d93-d4a5-48b3-804b-99c406555c89", address="1.2.3.4", hostname="foobar", ) nodemgr._start_ceph = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() await nodemgr.start() nodemgr._start_ceph.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore
def test_interrupt(mocker: MockerFixture) -> None: """ Test that ``CTRL-C`` stops the CLI. """ main = mocker.AsyncMock(side_effect=KeyboardInterrupt()) mocker.patch("nefelibata.console.main", main) _logger = mocker.patch("nefelibata.console._logger") console.run() _logger.info.assert_called_with("Stopping Nefelibata")
async def test_bootstrap_finisher_cb(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._init_stage = NodeInitStage.NONE assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.PREPARE assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.STARTED assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._save_state = mocker.AsyncMock() nodemgr._post_bootstrap_config = mocker.AsyncMock() await nodemgr._post_bootstrap_finisher(True, None) nodemgr._save_state.assert_called_once() # type: ignore nodemgr._post_bootstrap_config.assert_called_once() # type: ignore
async def test_finish_deployment_cb(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._init_stage = NodeInitStage.NONE assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.PREPARE assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.STARTED assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._deployment.finish_deployment = mocker.MagicMock() nodemgr._load = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() await nodemgr._finish_deployment(True, None) nodemgr._deployment.finish_deployment.assert_called_once() # type: ignore nodemgr._load.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore
async def test_obtain_images( gstate: GlobalState, mocker: MockerFixture ) -> None: orig_cephadm_pull_img = gstate.cephadm.pull_images gstate.cephadm.pull_images = mocker.AsyncMock() nodemgr = NodeMgr(gstate) ret = await nodemgr._obtain_images() assert ret gstate.cephadm.pull_images.assert_called_once() # type: ignore from gravel.cephadm.cephadm import CephadmError gstate.cephadm.pull_images = mocker.AsyncMock( side_effect=CephadmError("foobar") ) ret = await nodemgr._obtain_images() assert not ret gstate.cephadm.pull_images.assert_called_once() # type: ignore gstate.cephadm.pull_images = orig_cephadm_pull_img
async def test_announcer_collect( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test collecting interactions on posts and sites. """ gemini_builder = Builder(root, config, "gemini://example.com/", "gemini") html_builder = Builder(root, config, "https://example.com/", "www") Client = mocker.patch("nefelibata.announcers.geminispace.Client") Client.return_value.get = mocker.AsyncMock() Client.return_value.get.return_value.read.return_value = b""" ### 3 cross-capsule backlinks => gemini://example.com/reply.gmi Re: This is your first post => gemini://example.com/another-reply.gmi Re: This is your first post """ announcer = GeminispaceAnnouncer(root, config, builders=[gemini_builder]) # site collect is no-op interactions = await announcer.collect_site() assert interactions == {} interactions = await announcer.collect_post(post) assert interactions == { "backlink,gemini://example.com/reply.gmi": Interaction( id="backlink,gemini://example.com/reply.gmi", name="Re: This is your first post", url="gemini://example.com/reply.gmi", type="backlink", timestamp=None, ), "backlink,gemini://example.com/another-reply.gmi": Interaction( id="backlink,gemini://example.com/another-reply.gmi", name="Re: This is your first post", url="gemini://example.com/another-reply.gmi", type="backlink", timestamp=None, ), } announcer = GeminispaceAnnouncer(root, config, builders=[html_builder]) with pytest.raises(Exception) as excinfo: await announcer.collect_post(post) assert ( str(excinfo.value) == "Geminispace announcer only works with `gemini://` builds" )
async def test_node_start( gstate: GlobalState, mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, nodemgr: NodeMgr, ) -> None: from gravel.controllers.nodes.deployment import NodeStageEnum from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._deployment._state._stage = NodeStageEnum.READY nodemgr._obtain_state = mocker.AsyncMock() nodemgr._load = mocker.AsyncMock() nodemgr._incoming_msg_task = mocker.AsyncMock() nodemgr._connmgr.start_receiving = mocker.MagicMock() await nodemgr._node_start() assert nodemgr._init_stage == NodeInitStage.STARTED nodemgr._obtain_state.assert_called_once() # type: ignore nodemgr._load.assert_called_once() # type: ignore nodemgr._incoming_msg_task.assert_called_once() # type: ignore nodemgr._connmgr.start_receiving.assert_called_once() # type: ignore
async def test_assistant( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test the assistant. """ mocker.patch( "nefelibata.assistants.mirror_images.download_image", return_value=mocker.AsyncMock(), ) assistant = MirrorImagesAssistant(root, config) post.content = """ I have 2 images: - ![First image](https://example.com/photo.jpg) - ![Second image](img/logo.png) """ await assistant.process_post(post) await assistant.process_post(post) async def modify_replacements( # pylint: disable=too-many-arguments, unused-argument session: ClientSession, url: str, title: str, post: Post, mirror: Path, replacements: Dict[str, str], ) -> None: """ A dummy function to modify ``replacements``. """ replacements["hello"] = "world" mocker.patch( "nefelibata.assistants.mirror_images.download_image", modify_replacements, ) await assistant.process_post(post)
async def test_get_webmention_endpoint_unicode_error( mocker: MockerFixture) -> None: """ Test ``get_webmention_endpoint`` when response is not text. """ session = mocker.MagicMock() head_response = session.head.return_value.__aenter__.return_value head_response.headers = { "content-type": "text/html", } head_response.links = {} mocker.patch("nefelibata.announcers.webmention.UnicodeDecodeError", Exception) get_response = session.get.return_value.__aenter__.return_value get_response.text = mocker.AsyncMock() get_response.text.side_effect = Exception() endpoint = await get_webmention_endpoint( session, URL("https://example.com/post/hello.php"), ) assert endpoint is None
async def test_announcer_announce( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test announcing sites and posts. """ gemini_builder = Builder(root, config, "gemini://example.com/", "gemini") html_builder = Builder(root, config, "https://example.com/", "www") Client = mocker.patch("nefelibata.announcers.gemlog.Client") Client.return_value.get = mocker.AsyncMock() announcer = CAPCOMAnnouncer(root, config, builders=[gemini_builder]) # post announcement is no-op announcement = await announcer.announce_post(post) assert announcement is None with freeze_time("2021-01-01T00:00:00Z"): announcement = await announcer.announce_site() assert announcement.dict() == { "url": "gemini://gemini.circumlunar.space/capcom/", "timestamp": datetime(2021, 1, 1, 0, 0, tzinfo=timezone.utc), "grace_seconds": 31536000, } assert Client.return_value.get.called_with( URL( "gemini://gemini.circumlunar.space/capcom/submit?gemini://example.com/feed", ), ) announcer = CAPCOMAnnouncer(root, config, builders=[html_builder]) with pytest.raises(Exception) as excinfo: await announcer.announce_site() assert str( excinfo.value) == "CAPCOM announcer only works with `gemini://` builds"
async def test_main_new(mocker: MockerFixture) -> None: """ Test ``main`` with the "new" action. """ new = mocker.patch("nefelibata.console.new") new.run = mocker.AsyncMock() mocker.patch( "nefelibata.console.docopt", return_value={ "--loglevel": "debug", "init": False, "new": True, "build": False, "publish": False, "ROOT_DIR": "/path/to/blog", "POST": "A like", "-t": "like", "--force": False, }, ) await console.main() new.run.assert_called_with(Path("/path/to/blog"), "A like", "like")
async def test_main_canceled(mocker: MockerFixture) -> None: """ Test canceling the ``main`` coroutine. """ build = mocker.patch("nefelibata.console.build") build.run = mocker.AsyncMock( side_effect=asyncio.CancelledError("Canceled")) _logger = mocker.patch("nefelibata.console._logger") mocker.patch( "nefelibata.console.docopt", return_value={ "--loglevel": "debug", "init": False, "new": False, "build": True, "publish": False, "ROOT_DIR": "/path/to/blog", "--force": False, }, ) await console.main() _logger.info.assert_called_with("Canceled")
async def test_run( mocker: MockerFixture, root: Path, post: Post, ) -> None: """ Test ``run``. """ assistant = mocker.MagicMock() assistant.process_post = mocker.AsyncMock() assistant.process_site = mocker.AsyncMock() builder = mocker.MagicMock() builder.process_post = mocker.AsyncMock() builder.process_site = mocker.AsyncMock() announcer1 = mocker.MagicMock() announcer1.collect_post = mocker.AsyncMock(return_value={}) announcer1.collect_site = mocker.AsyncMock(return_value={}) announcer2 = mocker.MagicMock() announcer2.collect_site = mocker.AsyncMock(return_value={}) announcer2.collect_site.return_value = {} mocker.patch( "nefelibata.cli.build.get_announcers", return_value={ "announcer1": announcer1, "announcer2": announcer2 }, ) mocker.patch( "nefelibata.cli.build.get_assistants", return_value={"assistant": assistant}, ) mocker.patch("nefelibata.cli.build.get_builders", return_value={"builder": builder}) mocker.patch("nefelibata.cli.build.get_config") mocker.patch("nefelibata.cli.build.get_posts", return_value=[post]) _logger = mocker.patch("nefelibata.cli.build._logger") post.announcers = {"announcer1"} await build.run(root) assistant.process_post.assert_called_with(post, False) assistant.process_site.assert_called_with(False) builder.process_post.assert_called_with(post, False) builder.process_site.assert_called_with(False) announcer1.collect_post.assert_called_with(post) announcer1.collect_site.assert_called_with() announcer2.collect_post.assert_not_called() announcer2.collect_site.assert_called_with() _logger.info.assert_has_calls([ mocker.call("Building blog"), mocker.call("Creating `build/` directory"), mocker.call("Collecting interactions from posts"), mocker.call("Collecting interactions from site"), mocker.call("Running post assistants"), mocker.call("Running site assistants"), mocker.call("Processing posts"), mocker.call("Processing site"), ], ) _logger.reset_mock() await build.run(root) _logger.info.assert_has_calls([ mocker.call("Processing posts"), mocker.call("Processing site"), ], )
async def test_run( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test ``publish``. """ publisher = mocker.MagicMock() publisher.publish = mocker.AsyncMock(side_effect=[ Publishing(timestamp=datetime(2021, 1, 1)), None, Publishing(timestamp=datetime(2021, 1, 3)), ], ) announcer = mocker.MagicMock() announcer.announce_post = mocker.AsyncMock(side_effect=[ None, Announcement( url="https://host1.example.com/", timestamp=datetime(2021, 1, 1), ), ], ) announcer.announce_site = mocker.AsyncMock(side_effect=[ Announcement( url="https://host2.example.com/", timestamp=datetime(2021, 1, 2), ), None, ], ) mocker.patch( "nefelibata.cli.publish.get_announcers", return_value={"announcer": announcer}, ) mocker.patch( "nefelibata.cli.publish.get_publishers", return_value={"publisher": publisher}, ) # On the first publish we should announce site and post. await publish.run(root) publisher.publish.assert_called_with(None, False) announcer.announce_site.assert_called_with() announcer.announce_post.assert_called() # Publish again, should have a ``since`` value. Because ``publish`` # returns ``None`` the second time we shouldn't announce the site. # And because the first time ``publish_post`` returned ``None``, # it should try again now and be called. announcer.announce_site.reset_mock() announcer.announce_post.reset_mock() await publish.run(root) publisher.publish.assert_called_with(datetime(2021, 1, 1), False) announcer.announce_site.assert_not_called() announcer.announce_post.assert_called() # Publish again. This time ``publish`` returns a new data, so we # expect to call ``publish_site``. Because the post was already # published last time it shouldn't be announced this time. announcer.announce_post.reset_mock() await publish.run(root) publisher.publish.assert_called_with(datetime(2021, 1, 1), False) announcer.announce_site.assert_called_with() announcer.announce_post.assert_not_called()
def test_task( event_loop: asyncio.AbstractEventLoop, log_records: LogRecordsType, mocker: MockerFixture, opcua_client: OPCUAClient, subscription_success: bool, ) -> None: mocked_client = MagicMock() mocker.patch.object(opcua_client, "_create_opc_client", new=AsyncMock(return_value=mocked_client)) mocker.patch("opcua_webhmi_bridge.opcua.UaStatusCodeError", FakeUaStatusCodeError) type_node = mocker.sentinel.type_node mocked_client.get_namespace_index = mocker.AsyncMock( return_value=mocker.sentinel.ns) mocked_client.nodes.opc_binary.get_child = mocker.AsyncMock( return_value=type_node) mocked_client.load_type_definitions = mocker.AsyncMock() mocked_client.create_subscription = mocker.AsyncMock() subscription = mocked_client.create_subscription.return_value sub_results = [12, 34, 56, 78, 910] if not subscription_success: sub_results[-2] = mocker.Mock( **{"check.side_effect": FakeUaStatusCodeError}) subscription.subscribe_data_change = mocker.AsyncMock( return_value=sub_results) mocked_sleep: AsyncMock = mocker.patch("asyncio.sleep") gotten_node = mocked_client.get_node.return_value read_data_value = gotten_node.read_data_value = mocker.AsyncMock( side_effect=InfiniteLoopBreaker) cm: ContextManager[Any] if subscription_success: cm = contextlib.suppress(InfiniteLoopBreaker) else: cm = pytest.raises(FakeUaStatusCodeError) with cm: event_loop.run_until_complete(opcua_client._task()) assert mocked_client.__aenter__.await_count == 1 assert mocked_client.get_namespace_index.await_args_list == [ mocker.call(SIMATIC_NAMESPACE_URI) ] assert mocked_client.nodes.opc_binary.get_child.await_args_list == [ mocker.call("sentinel.ns:SimaticStructures") ] assert mocked_client.load_type_definitions.await_args_list == [ mocker.call([type_node]) ] get_node = cast(Mock, mocked_client.get_node) expected_get_node_calls = [ mocker.call("ns=sentinel.ns;s=monitornode1"), mocker.call("ns=sentinel.ns;s=monitornode2"), mocker.call("ns=sentinel.ns;s=recnode1"), mocker.call("ns=sentinel.ns;s=recnode2"), ] if subscription_success: expected_get_node_calls.append(mocker.call(2259)) assert get_node.call_args_list == expected_get_node_calls assert mocked_client.create_subscription.await_args_list == [ mocker.call(1000, opcua_client) ] assert subscription.subscribe_data_change.await_args_list == [ mocker.call([gotten_node, gotten_node, gotten_node, gotten_node]) ] if subscription_success: assert mocked_sleep.await_args_list == [mocker.call(5)] assert read_data_value.await_args_list == [mocker.call()] else: last_log_record = log_records()[-1] assert last_log_record.levelno == logging.ERROR assert "Error subscribing to node" in last_log_record.message
def __init__(self, mocker: pytest_mock.MockerFixture) -> None: self.get_passage = mocker.AsyncMock() self.search = mocker.AsyncMock()
async def test_announcer_announce( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test announcing posts. """ gemini_builder = Builder(root, config, "gemini://example.com/", "gemini") gemini_builder.extension = ".gmi" html_builder = Builder(root, config, "https://example.com/", "www") html_builder.extension = ".html" send_webmention = mocker.AsyncMock(side_effect=[ Webmention( source="gemini://example.com/first/index.html", target="https://nefelibata.readthedocs.io/", status="invalid", ), Webmention( source="https://example.com/first/index.html", target="https://nefelibata.readthedocs.io/", status="queue", location="https://bob.example.com/webmention.php?id=42", ), ], ) mocker.patch("nefelibata.announcers.webmention.send_webmention", send_webmention) announcer = WebmentionAnnouncer( root, config, builders=[gemini_builder, html_builder], ) announcement = await announcer.announce_post(post) assert announcement is None path = post.path.parent / "webmentions.yaml" with open(path, encoding="utf-8") as input_: webmentions = yaml.load(input_, Loader=yaml.SafeLoader) assert webmentions == { "gemini://example.com/first/index.gmi => https://nefelibata.readthedocs.io/": { "location": None, "source": "gemini://example.com/first/index.html", "status": "invalid", "target": "https://nefelibata.readthedocs.io/", }, "https://example.com/first/index.html => https://nefelibata.readthedocs.io/": { "location": "https://bob.example.com/webmention.php?id=42", "source": "https://example.com/first/index.html", "status": "queue", "target": "https://nefelibata.readthedocs.io/", }, } update_webmention = mocker.AsyncMock(return_value=Webmention( source="https://example.com/first/index.html", target="https://nefelibata.readthedocs.io/", status="success", location=None, ), ) mocker.patch( "nefelibata.announcers.webmention.update_webmention", update_webmention, ) with freeze_time("2021-01-01T00:00:00Z"): announcement = await announcer.announce_post(post) assert announcement == Announcement( url="first/index", timestamp=datetime(2021, 1, 1, tzinfo=timezone.utc), grace_seconds=0, ) path = post.path.parent / "webmentions.yaml" with open(path, encoding="utf-8") as input_: webmentions = yaml.load(input_, Loader=yaml.SafeLoader) assert webmentions == { "gemini://example.com/first/index.gmi => https://nefelibata.readthedocs.io/": { "location": None, "source": "gemini://example.com/first/index.html", "status": "invalid", "target": "https://nefelibata.readthedocs.io/", }, "https://example.com/first/index.html => https://nefelibata.readthedocs.io/": { "location": None, "source": "https://example.com/first/index.html", "status": "success", "target": "https://nefelibata.readthedocs.io/", }, }
async def test_enable( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, ) -> None: from gravel.controllers.nodes.systemdisk import ( MountError, OverlayError, SystemDisk, ) async def mount_fail() -> None: raise MountError("Failed mount.") overlayed_paths = [] bindmounts = [] async def mock_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: if cmd[2] == "overlay": assert "lower" in cmd[4] lowerstr = (cmd[4].split(","))[0] lower = (lowerstr.split("="))[1] overlayed_paths.append(lower) elif cmd[1] == "--bind": assert len(cmd) == 4 bindmounts.append(cmd[3]) else: raise Exception(f"Unknown call: {cmd}") return 0, None, None # ensure we don't have a mounted fs fs.add_real_file( source_path=os.path.join(DATA_DIR, "mounts_without_aquarium.raw"), target_path="/proc/mounts", ) systemdisk = SystemDisk(gstate) assert not systemdisk.mounted systemdisk.mount = mount_fail mocker.patch( "gravel.controllers.nodes.systemdisk.aqr_run_cmd", new=mock_call ) throws = False try: await systemdisk.enable() except OverlayError as e: assert "failed mount" in e.message.lower() throws = True assert throws systemdisk.mount = mocker.AsyncMock() for upper in systemdisk._overlaydirs.keys(): fs.create_dir(f"/var/lib/aquarium-system/{upper}/overlay") fs.create_dir(f"/var/lib/aquarium-system/{upper}/temp") for ours in systemdisk._bindmounts.keys(): fs.create_dir(f"/var/lib/aquarium-system/{ours}") await systemdisk.enable() for lower in systemdisk._overlaydirs.values(): assert fs.exists(lower) assert lower in overlayed_paths for theirs in systemdisk._bindmounts.values(): assert fs.exists(theirs) assert theirs in bindmounts
async def test_get_webmention_endpoint(mocker: MockerFixture) -> None: """ Test ``get_webmention_endpoint``. """ session = mocker.MagicMock() head_response = session.head.return_value.__aenter__.return_value head_response.headers = { "content-type": "text/html", } head_response.links = { "alternate": { "url": "/atom.xml", }, "webmention": { "url": "/webmention.php", }, } get_response = session.get.return_value.__aenter__.return_value get_response.text = mocker.AsyncMock() get_response.text.return_value = """<!doctype html> <html lang="en"> <head> <link rel="webmention" href="/webmention.php" /> </head> <body> </body> </html>""" # endpoint in Link header endpoint = await get_webmention_endpoint( session, URL("https://example.com/post/hello.php"), ) assert endpoint == URL("https://example.com/webmention.php") # endpoint in HTML head_response.links = {} endpoint = await get_webmention_endpoint( session, URL("https://example.com/post/hello.php"), ) assert endpoint == URL("https://example.com/webmention.php") # no endpoint in HTML get_response.text.return_value = """<!doctype html> <html lang="en"> <head> </head> <body> </body> </html>""" endpoint = await get_webmention_endpoint( session, URL("https://example.com/post/hello.php"), ) assert endpoint is None # unable to extract endpoint head_response.headers = { "content-type": "application/pdf", } endpoint = await get_webmention_endpoint( session, URL("https://example.com/post/hello.php"), ) assert endpoint is None # gemini target head_response.links = {} endpoint = await get_webmention_endpoint( session, URL("gemini://example.com/post/hello.gmi"), ) assert endpoint is None
async def test_announcer_collect( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test collecting interactions on posts and sites. """ builder = Builder(root, config, "gemini://example.com/", "gemini") Client = mocker.patch("nefelibata.announcers.gemlog.Client") Client.return_value.get = mocker.AsyncMock() Client.return_value.get.return_value.read.side_effect = [ b""" => gemini://example.com/reply.gmi Re: This is your first post """, b""" => gemini://example.com/first/index.gmi """, ] announcer = CAPCOMAnnouncer(root, config, builders=[builder]) # post collect is no-op interactions = await announcer.collect_post(post) assert interactions == {} interactions = await announcer.collect_site() assert interactions == { Path("/path/to/blog/posts/first/index.mkd"): { "reply,gemini://example.com/reply.gmi": Interaction( id="reply,gemini://example.com/reply.gmi", name="Re: This is your first post", url="gemini://example.com/reply.gmi", type="reply", timestamp=None, ), }, } # test no backlink Client.return_value.get.return_value.read.side_effect = [ b""" => gemini://example.com/reply.gmi Re: This is your first post """, b"", ] announcer = CAPCOMAnnouncer(root, config, builders=[builder]) interactions = await announcer.collect_site() assert interactions == {} # test SSL error response = mocker.AsyncMock() response.read.return_value = b""" => gemini://example.com/reply.gmi Re: This is your first post """ Client.return_value.get.side_effect = [ response, ssl.SSLCertVerificationError("A wild error appears!"), ] announcer = CAPCOMAnnouncer(root, config, builders=[builder]) interactions = await announcer.collect_site() assert interactions == { Path("/path/to/blog/posts/first/index.mkd"): { "reply,gemini://example.com/reply.gmi": Interaction( id="reply,gemini://example.com/reply.gmi", name="Re: This is your first post", url="gemini://example.com/reply.gmi", type="reply", timestamp=None, ), }, }
async def test_announcer_collect( mocker: MockerFixture, root: Path, config: Config, post: Post, ) -> None: """ Test collecting posts. """ gemini_builder = Builder(root, config, "gemini://example.com/", "gemini") gemini_builder.extension = ".gmi" html_builder = Builder(root, config, "https://example.com/", "www") html_builder.extension = ".html" mocker.patch("nefelibata.announcers.webmention.ClientResponseError", Exception) get = mocker.patch("nefelibata.announcers.webmention.ClientSession.get") get_response = get.return_value.__aenter__.return_value get_response.raise_for_status = mocker.MagicMock() get_response.raise_for_status.side_effect = [ Exception("Gemini not supported"), None, Exception("Gemini not supported"), None, ] announcer = WebmentionAnnouncer( root, config, builders=[gemini_builder, html_builder], ) get_response.json = mocker.AsyncMock(return_value={"children": []}) interactions = await announcer.collect_post(post) assert interactions == {} entries = [ { "type": "entry", "wm-id": 1, "wm-source": "https://alice.example.com/posts/one", "name": "This is the title", "summary": { "value": "This is the summary" }, "content": { "text": "This is the post content." }, "published": "2021-01-01T00:00:00Z", "author": { "name": "Alice Doe", "url": "https://alice.example.com/", "photo": "https://alice.example.com/photo.jpg", "note": "My name is Alice", }, "wm-property": "in-reply-to", }, { "type": "entry", "wm-id": 2, "wm-source": "https://bob.example.com/posts/two", "summary": { "value": "This is the summary" }, "content": { "text": "This is the post content." }, "published": "2021-01-01T00:00:00Z", "author": { "name": "Bob Doe", "url": "https://bob.example.com/", "photo": "https://bob.example.com/photo.jpg", }, "wm-property": "like-of", }, { "type": "invalid" }, ] get_response.json = mocker.AsyncMock(return_value={"children": entries}) interactions = await announcer.collect_post(post) assert interactions == { 1: Interaction( id="1", name="This is the title", summary="This is the summary", content="This is the post content.", published=datetime(2021, 1, 1, 0, 0, tzinfo=timezone.utc), updated=None, author=Author( name="Alice Doe", url="https://alice.example.com/", avatar="https://alice.example.com/photo.jpg", note="My name is Alice", ), url="https://alice.example.com/posts/one", in_reply_to=None, type="reply", ), 2: Interaction( id="2", name="https://bob.example.com/posts/two", summary="This is the summary", content="This is the post content.", published=datetime(2021, 1, 1, 0, 0, tzinfo=timezone.utc), updated=None, author=Author( name="Bob Doe", url="https://bob.example.com/", avatar="https://bob.example.com/photo.jpg", note="", ), url="https://bob.example.com/posts/two", in_reply_to=None, type="like", ), }
async def test_join(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from uuid import UUID from gravel.controllers.inventory.disks import DiskDevice from gravel.controllers.nodes.deployment import DeploymentDisksConfig from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import JoinParamsModel, NodeInitStage def mock_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution( systemdisk=DiskDevice( id="foo01", name="foo", path="/dev/foo", product="Foo", vendor="Foo Inc", size=1000, rotational=False, available=True, rejected_reasons=[], ), storage=[ DiskDevice( id="bar01", name="bar", path="/dev/bar", product="Bar", vendor="Bar LLC", size=2000, rotational=False, available=True, rejected_reasons=[], ), DiskDevice( id="baz01", name="baz", path="/dev/baz", product="Baz", vendor="Baz Ltd", size=2000, rotational=False, available=True, rejected_reasons=[], ), ], storage_size=4000, possible=True, ) async def mock_join( leader_address: str, token: str, uuid: UUID, hostname: str, address: str, disks: DeploymentDisksConfig, ) -> bool: assert leader_address == "10.1.2.3" assert token == "751b-51fd-10d7-f7b4" assert str(uuid) == "bba35d93-d4a5-48b3-804b-99c406555c89" assert hostname == "foobar" assert address == "1.2.3.4" assert disks.system == "/dev/foo" assert len(disks.storage) == 2 assert "/dev/bar" in disks.storage assert "/dev/baz" in disks.storage return True mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=mock_solution) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._deployment.join = mocker.AsyncMock(side_effects=Exception()) throws = False try: await nodemgr.join( "10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar"), ) except Exception: throws = True assert throws nodemgr._deployment.join.assert_called_once() # type: ignore nodemgr._deployment.join = mocker.AsyncMock(return_value=False) res = await nodemgr.join("10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar")) assert not res nodemgr._deployment.join.assert_called_once() # type: ignore nodemgr._save_state = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() nodemgr._deployment.join = mock_join res = await nodemgr.join("10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar")) assert res assert nodemgr._token == "751b-51fd-10d7-f7b4" nodemgr._save_state.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore
async def test_deploy(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.auth import UserMgr, UserModel from gravel.controllers.inventory.disks import DiskDevice from gravel.controllers.nodes.deployment import DeploymentConfig from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import NodeInitStage called_mock_deploy = False def mock_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution( systemdisk=DiskDevice( id="foo01", name="foo", path="/dev/foo", product="Foo", vendor="Foo Inc", size=1000, rotational=False, available=True, rejected_reasons=[], ), storage=[ DiskDevice( id="bar01", name="bar", path="/dev/bar", product="Bar", vendor="Bar LLC", size=2000, rotational=False, available=True, rejected_reasons=[], ), DiskDevice( id="baz01", name="baz", path="/dev/baz", product="Baz", vendor="Baz Ltd", size=2000, rotational=False, available=True, rejected_reasons=[], ), ], storage_size=4000, possible=True, ) async def mock_deploy( config: DeploymentConfig, post_bootstrap_cb: Callable[[bool, Optional[str]], Awaitable[None]], finisher: Callable[[bool, Optional[str]], Awaitable[None]], ) -> None: import inspect nonlocal called_mock_deploy called_mock_deploy = True assert config.hostname == "barbaz" assert config.address == "1.2.3.4" assert config.token == "751b-51fd-10d7-f7b4" assert config.ntp_addr == "my.ntp.addr" assert config.disks.system == "/dev/foo" assert len(config.disks.storage) == 2 assert "/dev/bar" in config.disks.storage assert "/dev/baz" in config.disks.storage assert post_bootstrap_cb is not None assert finisher is not None assert inspect.iscoroutinefunction(post_bootstrap_cb) assert inspect.iscoroutinefunction(finisher) mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=mock_solution) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._generate_token = mocker.MagicMock( return_value="751b-51fd-10d7-f7b4") nodemgr._save_token = mocker.AsyncMock() nodemgr._deployment.deploy = mock_deploy await gstate.store.ensure_connection() await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr")) assert called_mock_deploy assert nodemgr._token == "751b-51fd-10d7-f7b4" assert nodemgr._state.hostname == "barbaz" nodemgr._save_token.assert_called_once() # type: ignore ntpaddr = await gstate.store.get("/nodes/ntp_addr") assert ntpaddr == "my.ntp.addr" usermgr = UserMgr(gstate.store) assert await usermgr.exists("admin") user: Optional[UserModel] = await usermgr.get("admin") assert user is not None assert user.username == "admin" # We can't test the plain password here because it will fail # and we don't care particularly about the password itself, just that # the user has been populated. We'll leave for the 'UserMgr' tests to # validate the correctness of its operations. assert len(user.password) > 0