These switches have no CLI at all — no SSH, no Telnet and no ArubaOS-Switch REST API — so the ProCurve driver cannot serve them despite the shared vendor. The only management surface is the web UI, which ships its table data as JavaScript array literals; those parse with ast.literal_eval, so the driver needs no HTML parser and no dependency beyond napalm/requests. Read-only by design: the platform exposes a single administrator account with no privilege levels, and serves HTTPS only after a certificate has been uploaded, so the polling credential is necessarily the admin credential over a plain channel. Implements get_facts, get_interfaces, get_vlans, get_vlans_detail and get_mac_address_table, plus HTTP/SNMP fingerprints for discovery. Tested against an HPE OfficeConnect 1820 8G PoE+ (65W), J9982A, PT.02.19.
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""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="<html>data</html>")
|
|
|
|
assert client.fetch("/htdocs/pages/base/dashboard.lsp") == "<html>data</html>"
|
|
|
|
|
|
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()
|