"""Unit tests for the OfficeConnect HTTP client — no real device required.""" from unittest.mock import MagicMock, patch import pytest from napalm.base.exceptions import ConnectionClosedException, ConnectionException from napalm_hpe_officeconnect.client import OfficeConnectClient def make_response(status_code=200, text="", json_data=None): resp = MagicMock() resp.status_code = status_code resp.text = text if json_data is None: resp.json.side_effect = ValueError("no json") else: resp.json.return_value = json_data return resp @pytest.fixture def client(): with patch("napalm_hpe_officeconnect.client.requests.Session") as session_cls: c = OfficeConnectClient("192.0.2.20", "admin", "secret") c._session = session_cls.return_value yield c # =========================================================================== # base_url # =========================================================================== def test_base_url_defaults_to_http(): """These switches ship without a certificate, so HTTP is the default.""" c = OfficeConnectClient("192.0.2.20", "admin", "secret") assert c.base_url == "http://192.0.2.20" def test_base_url_honours_https_opt_in(): c = OfficeConnectClient("192.0.2.20", "admin", "secret", scheme="https") assert c.base_url == "https://192.0.2.20" def test_base_url_includes_non_default_port(): c = OfficeConnectClient("192.0.2.20", "admin", "secret", port=8080) assert c.base_url == "http://192.0.2.20:8080" # =========================================================================== # login # =========================================================================== def test_login_posts_credentials_and_succeeds(client): client._session.get.return_value = make_response(text="login page") client._session.post.return_value = make_response( json_data={"redirect": "/htdocs/pages/main/main.lsp", "error": ""} ) client.login() # The RID cookie is handed out by the login page, so it must be fetched first. client._session.get.assert_called_once() assert client._session.get.call_args[0][0].endswith("/htdocs/login/login.lsp") (url,) = client._session.post.call_args[0] assert url.endswith("/htdocs/login/login.lua") assert client._session.post.call_args[1]["data"] == { "username": "admin", "password": "secret", } assert client.is_authenticated is True def test_login_raises_with_device_message_on_bad_credentials(client): client._session.get.return_value = make_response(text="login page") client._session.post.return_value = make_response( json_data={"redirect": "", "error": "Invalid username or password"} ) with pytest.raises(ConnectionException, match="Invalid username or password"): client.login() assert client.is_authenticated is False def test_login_raises_when_response_is_not_json(client): """A firmware that redirects instead of answering must not look like success.""" client._session.get.return_value = make_response(text="login page") client._session.post.return_value = make_response(status_code=303, text="") with pytest.raises(ConnectionException): client.login() assert client.is_authenticated is False # =========================================================================== # fetch # =========================================================================== def test_fetch_returns_page_text(client): client._authenticated = True client._session.get.return_value = make_response(text="data") assert client.fetch("/htdocs/pages/base/dashboard.lsp") == "data" def test_fetch_raises_when_session_expired(client): """An expired session answers 303 to the login page rather than 401.""" client._authenticated = True client._session.get.return_value = make_response(status_code=303, text="") with pytest.raises(ConnectionClosedException): client.fetch("/htdocs/pages/base/dashboard.lsp") def test_fetch_requires_login_first(client): with pytest.raises(ConnectionException): client.fetch("/htdocs/pages/base/dashboard.lsp") # =========================================================================== # logout — must be reliable, the session table is small # =========================================================================== def test_logout_releases_the_session(client): client._authenticated = True client._session.get.return_value = make_response(status_code=303) client.logout() assert client._session.get.call_args[0][0].endswith("/htdocs/pages/main/logout.lsp") assert client.is_authenticated is False def test_logout_swallows_errors(client): """close() runs logout on the failure path too; it must never mask the original exception, and an unreachable switch is not a new problem.""" client._authenticated = True client._session.get.side_effect = OSError("network gone") client.logout() # must not raise assert client.is_authenticated is False def test_logout_is_a_noop_when_not_logged_in(client): client.logout() client._session.get.assert_not_called()