mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-06-29 14:31:51 +02:00
f8c2a7bb40
- Rename ChannelVisibility.PUBLIC to ChannelVisibility.COMMUNITY - Update stored value from 'public' to 'community' across model, schema, API, CLI, and frontend - Add Alembic migration to update existing database rows - Consolidate upgrade docs: merge v0.11.0, v0.12.0, v0.13.0 into single v0.11.0 section - Add i18n visibility level translation keys (en, nl) - Update section headings on channels page to use t() for i18n - Keep visibility badges lowercase per UI design
313 lines
11 KiB
Python
313 lines
11 KiB
Python
"""Tests for channel_visibility helpers."""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from meshcore_hub.api.channel_visibility import (
|
|
get_all_known_channel_indices,
|
|
get_max_visibility_level,
|
|
get_visible_channel_indices,
|
|
resolve_user_role,
|
|
)
|
|
from meshcore_hub.common.models import Base
|
|
from meshcore_hub.common.models.channel import Channel
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
"""Create an in-memory SQLite database session."""
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
yield session
|
|
session.close()
|
|
Base.metadata.drop_all(engine)
|
|
engine.dispose()
|
|
|
|
|
|
def _make_request(
|
|
headers: dict | None = None, app_state: dict | None = None
|
|
) -> MagicMock:
|
|
"""Create a mock FastAPI Request."""
|
|
from types import SimpleNamespace
|
|
|
|
request = MagicMock()
|
|
request.headers = headers or {}
|
|
state = SimpleNamespace(**(app_state or {}))
|
|
request.app.state = state
|
|
return request
|
|
|
|
|
|
class TestResolveUserRole:
|
|
"""Tests for resolve_user_role()."""
|
|
|
|
def test_no_header_returns_none(self) -> None:
|
|
"""No X-User-Roles header returns None."""
|
|
request = _make_request(headers={})
|
|
assert resolve_user_role(request) is None
|
|
|
|
def test_empty_header_returns_none(self) -> None:
|
|
"""Empty X-User-Roles header returns None."""
|
|
request = _make_request(headers={"x-user-roles": ""})
|
|
assert resolve_user_role(request) is None
|
|
|
|
def test_admin_role(self) -> None:
|
|
"""Admin role is resolved correctly."""
|
|
request = _make_request(headers={"x-user-roles": "admin"})
|
|
assert resolve_user_role(request) == "admin"
|
|
|
|
def test_operator_role(self) -> None:
|
|
"""Operator role is resolved correctly."""
|
|
request = _make_request(headers={"x-user-roles": "operator"})
|
|
assert resolve_user_role(request) == "operator"
|
|
|
|
def test_member_role(self) -> None:
|
|
"""Member role is resolved correctly."""
|
|
request = _make_request(headers={"x-user-roles": "member"})
|
|
assert resolve_user_role(request) == "member"
|
|
|
|
def test_admin_takes_precedence_over_member(self) -> None:
|
|
"""Admin takes precedence when multiple roles present."""
|
|
request = _make_request(headers={"x-user-roles": "member,admin"})
|
|
assert resolve_user_role(request) == "admin"
|
|
|
|
def test_operator_takes_precedence_over_member(self) -> None:
|
|
"""Operator takes precedence over member."""
|
|
request = _make_request(headers={"x-user-roles": "member,operator"})
|
|
assert resolve_user_role(request) == "operator"
|
|
|
|
def test_admin_takes_precedence_over_all(self) -> None:
|
|
"""Admin takes precedence over operator and member."""
|
|
request = _make_request(headers={"x-user-roles": "member,operator,admin"})
|
|
assert resolve_user_role(request) == "admin"
|
|
|
|
def test_unknown_role_returns_none(self) -> None:
|
|
"""Unknown role returns None."""
|
|
request = _make_request(headers={"x-user-roles": "viewer"})
|
|
assert resolve_user_role(request) is None
|
|
|
|
def test_custom_role_names(self) -> None:
|
|
"""Custom OIDC role names from app.state are recognized."""
|
|
request = _make_request(
|
|
headers={"x-user-roles": "superadmin,moderator"},
|
|
app_state={
|
|
"oidc_role_admin": "superadmin",
|
|
"oidc_role_operator": "moderator",
|
|
"oidc_role_member": "user",
|
|
},
|
|
)
|
|
assert resolve_user_role(request) == "admin"
|
|
|
|
def test_custom_member_role_name(self) -> None:
|
|
"""Custom member role name is recognized."""
|
|
request = _make_request(
|
|
headers={"x-user-roles": "user"},
|
|
app_state={
|
|
"oidc_role_member": "user",
|
|
},
|
|
)
|
|
assert resolve_user_role(request) == "member"
|
|
|
|
def test_whitespace_in_header(self) -> None:
|
|
"""Whitespace around role names is handled."""
|
|
request = _make_request(headers={"x-user-roles": " admin , member "})
|
|
assert resolve_user_role(request) == "admin"
|
|
|
|
|
|
class TestGetMaxVisibilityLevel:
|
|
"""Tests for get_max_visibility_level()."""
|
|
|
|
def test_none_returns_zero(self) -> None:
|
|
"""Anonymous users get level 0 (community only)."""
|
|
assert get_max_visibility_level(None) == 0
|
|
|
|
def test_community_returns_zero(self) -> None:
|
|
assert get_max_visibility_level("community") == 0
|
|
|
|
def test_member_returns_one(self) -> None:
|
|
assert get_max_visibility_level("member") == 1
|
|
|
|
def test_operator_returns_two(self) -> None:
|
|
assert get_max_visibility_level("operator") == 2
|
|
|
|
def test_admin_returns_three(self) -> None:
|
|
assert get_max_visibility_level("admin") == 3
|
|
|
|
def test_unknown_returns_zero(self) -> None:
|
|
assert get_max_visibility_level("unknown") == 0
|
|
|
|
|
|
class TestGetVisibleChannelIndices:
|
|
"""Tests for get_visible_channel_indices()."""
|
|
|
|
def test_always_includes_idx_17(self, db_session) -> None:
|
|
"""Built-in Public channel (idx 17) is always visible."""
|
|
indices = get_visible_channel_indices(db_session, 0)
|
|
assert 17 in indices
|
|
|
|
def test_community_channels_visible_at_level_0(self, db_session) -> None:
|
|
"""Community channels are visible at level 0."""
|
|
key = "AABBCCDDEEFF00112233445566778899"
|
|
ch = Channel(
|
|
name="Community",
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility="community",
|
|
)
|
|
db_session.add(ch)
|
|
db_session.commit()
|
|
|
|
indices = get_visible_channel_indices(db_session, 0)
|
|
expected_idx = int(ch.channel_hash, 16)
|
|
assert expected_idx in indices
|
|
assert 17 in indices
|
|
|
|
def test_member_channels_hidden_at_level_0(self, db_session) -> None:
|
|
"""Member channels are hidden at level 0."""
|
|
key = "11223344556677889900AABBCCDDEEFF"
|
|
ch = Channel(
|
|
name="MembersOnly",
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility="member",
|
|
)
|
|
db_session.add(ch)
|
|
db_session.commit()
|
|
|
|
indices = get_visible_channel_indices(db_session, 0)
|
|
ch_idx = int(ch.channel_hash, 16)
|
|
assert ch_idx not in indices
|
|
|
|
def test_member_channels_visible_at_level_1(self, db_session) -> None:
|
|
"""Member channels are visible at level 1."""
|
|
key = "11223344556677889900AABBCCDDEEFF"
|
|
ch = Channel(
|
|
name="MemberCh",
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility="member",
|
|
)
|
|
db_session.add(ch)
|
|
db_session.commit()
|
|
|
|
indices = get_visible_channel_indices(db_session, 1)
|
|
ch_idx = int(ch.channel_hash, 16)
|
|
assert ch_idx in indices
|
|
|
|
def test_admin_channels_visible_at_level_3(self, db_session) -> None:
|
|
"""Admin channels are visible at level 3."""
|
|
key = "FFEEDDCCBBAA99887766554433221100"
|
|
ch = Channel(
|
|
name="AdminCh",
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility="admin",
|
|
)
|
|
db_session.add(ch)
|
|
db_session.commit()
|
|
|
|
indices = get_visible_channel_indices(db_session, 3)
|
|
ch_idx = int(ch.channel_hash, 16)
|
|
assert ch_idx in indices
|
|
|
|
def test_admin_channels_hidden_at_level_1(self, db_session) -> None:
|
|
"""Admin channels are hidden at level 1."""
|
|
key = "FFEEDDCCBBAA99887766554433221100"
|
|
ch = Channel(
|
|
name="AdminCh",
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility="admin",
|
|
)
|
|
db_session.add(ch)
|
|
db_session.commit()
|
|
|
|
indices = get_visible_channel_indices(db_session, 1)
|
|
ch_idx = int(ch.channel_hash, 16)
|
|
assert ch_idx not in indices
|
|
|
|
def test_mixed_visibility_channels(self, db_session) -> None:
|
|
"""Multiple channels with different visibility levels."""
|
|
pub_key = "AABBCCDDEEFF00112233445566778899"
|
|
mem_key = "11223344556677889900AABBCCDDEEFF"
|
|
adm_key = "FFEEDDCCBBAA99887766554433221100"
|
|
|
|
for name, key, vis in [
|
|
("Community", pub_key, "community"),
|
|
("Member", mem_key, "member"),
|
|
("Admin", adm_key, "admin"),
|
|
]:
|
|
db_session.add(
|
|
Channel(
|
|
name=name,
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
visibility=vis,
|
|
)
|
|
)
|
|
db_session.commit()
|
|
|
|
level_0 = get_visible_channel_indices(db_session, 0)
|
|
level_1 = get_visible_channel_indices(db_session, 1)
|
|
level_3 = get_visible_channel_indices(db_session, 3)
|
|
|
|
pub_idx = int(Channel.compute_channel_hash(pub_key), 16)
|
|
mem_idx = int(Channel.compute_channel_hash(mem_key), 16)
|
|
adm_idx = int(Channel.compute_channel_hash(adm_key), 16)
|
|
|
|
assert pub_idx in level_0
|
|
assert mem_idx not in level_0
|
|
assert adm_idx not in level_0
|
|
|
|
assert pub_idx in level_1
|
|
assert mem_idx in level_1
|
|
assert adm_idx not in level_1
|
|
|
|
assert pub_idx in level_3
|
|
assert mem_idx in level_3
|
|
assert adm_idx in level_3
|
|
|
|
assert 17 in level_0
|
|
assert 17 in level_1
|
|
assert 17 in level_3
|
|
|
|
|
|
class TestGetAllKnownChannelIndices:
|
|
"""Tests for get_all_known_channel_indices()."""
|
|
|
|
def test_empty_db(self, db_session) -> None:
|
|
"""Empty DB returns empty set."""
|
|
indices = get_all_known_channel_indices(db_session)
|
|
assert indices == set()
|
|
|
|
def test_returns_all_indices(self, db_session) -> None:
|
|
"""Returns all channel indices from DB."""
|
|
key1 = "AABBCCDDEEFF00112233445566778899"
|
|
key2 = "11223344556677889900AABBCCDDEEFF"
|
|
for name, key in [("Ch1", key1), ("Ch2", key2)]:
|
|
db_session.add(
|
|
Channel(
|
|
name=name,
|
|
key_hex=key,
|
|
channel_hash=Channel.compute_channel_hash(key),
|
|
)
|
|
)
|
|
db_session.commit()
|
|
|
|
indices = get_all_known_channel_indices(db_session)
|
|
idx1 = int(Channel.compute_channel_hash(key1), 16)
|
|
idx2 = int(Channel.compute_channel_hash(key2), 16)
|
|
assert indices == {idx1, idx2}
|
|
|
|
def test_does_not_include_builtin_17(self, db_session) -> None:
|
|
"""Does not include the built-in Public channel (17) unless in DB."""
|
|
indices = get_all_known_channel_indices(db_session)
|
|
assert 17 not in indices
|