Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
asyncio_mode = auto
Empty file added tests/__init__.py
Empty file.
144 changes: 144 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Shared fixtures for magma pytest suite."""

import os
import sys
import types
import logging
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio

Comment on lines +6 to +11
# ---------------------------------------------------------------------------
# Ensure the project root is on sys.path
# ---------------------------------------------------------------------------
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)

# ---------------------------------------------------------------------------
# Stub out the Caldera-core imports that magma relies on at import time so
# the test suite can run without a full Caldera installation.
#
# magma's app/ directory is NOT a standalone package — in Caldera, "app" is
# the core framework package. magma_api.py does `from app.service.auth_svc
# import …` expecting Caldera's auth_svc. We create a synthetic "app"
# package whose __path__ includes the real app/ dir so that magma_api and
# magma_svc can be imported, while also providing stubs for the Caldera-core
# sub-modules they reference.
# ---------------------------------------------------------------------------

def _install_caldera_stubs():
"""Create minimal stand-ins for caldera packages that magma imports."""

app_dir = os.path.join(PROJECT_ROOT, "app")

# --- top-level "app" package with __path__ pointing at real dir --------
app_pkg = types.ModuleType("app")
app_pkg.__path__ = [app_dir]
app_pkg.__package__ = "app"

# --- app.utility.base_world -------------------------------------------
app_utility = types.ModuleType("app.utility")
app_utility.__path__ = []
app_utility.__package__ = "app.utility"

base_world_mod = types.ModuleType("app.utility.base_world")

class _Access:
APP = "app"
RED = "red"
BLUE = "blue"

class BaseWorld:
Access = _Access

base_world_mod.BaseWorld = BaseWorld

# --- app.service.auth_svc ---------------------------------------------
app_service = types.ModuleType("app.service")
app_service.__path__ = []
app_service.__package__ = "app.service"

auth_svc_mod = types.ModuleType("app.service.auth_svc")

def check_authorization(func):
return func

def for_all_public_methods(decorator):
def wrapper(cls):
return cls
return wrapper

auth_svc_mod.check_authorization = check_authorization
auth_svc_mod.for_all_public_methods = for_all_public_methods

# --- register them in sys.modules so later imports resolve ------------
mods = {
"app": app_pkg,
"app.utility": app_utility,
"app.utility.base_world": base_world_mod,
"app.service": app_service,
"app.service.auth_svc": auth_svc_mod,
}
for name, mod in mods.items():
sys.modules[name] = mod


_install_caldera_stubs()

# We also need `plugins.magma` to resolve for hook.py's relative import.
_plugins = types.ModuleType("plugins")
_plugins.__path__ = []
_plugins_magma = types.ModuleType("plugins.magma")
_plugins_magma.__path__ = [PROJECT_ROOT]
_plugins_magma_app = types.ModuleType("plugins.magma.app")
_plugins_magma_app.__path__ = [os.path.join(PROJECT_ROOT, "app")]

sys.modules["plugins"] = _plugins
sys.modules["plugins.magma"] = _plugins_magma
sys.modules["plugins.magma.app"] = _plugins_magma_app
Comment on lines +31 to +100

# Now import the actual modules under test — the stubs make this safe.
from app.magma_api import MagmaAPI # noqa: E402
from app.magma_svc import MagmaService # noqa: E402

# Patch plugins.magma.app.magma_api so hook.py's import succeeds
_pm_api = types.ModuleType("plugins.magma.app.magma_api")
_pm_api.MagmaAPI = MagmaAPI
_plugins_magma_app.magma_api = _pm_api
sys.modules["plugins.magma.app.magma_api"] = _pm_api


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def mock_services():
"""Return a dict of mock Caldera core services."""
return {
"auth_svc": MagicMock(name="auth_svc"),
"data_svc": MagicMock(name="data_svc"),
"file_svc": MagicMock(name="file_svc"),
"app_svc": MagicMock(name="app_svc"),
}


@pytest.fixture
def magma_api(mock_services):
return MagmaAPI(mock_services)


@pytest.fixture
def magma_svc(mock_services):
return MagmaService(mock_services)


@pytest.fixture
def mock_app(mock_services):
"""Fake aiohttp application (a plain dict is sufficient for router ops)."""
app = MagicMock()
app.router = MagicMock()
mock_services["app_svc"].application = app
return app
175 changes: 175 additions & 0 deletions tests/test_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""Tests for hook.py — plugin metadata and the enable() lifecycle."""

import importlib
import sys
import types
from unittest.mock import AsyncMock, MagicMock, patch
Comment on lines +3 to +6

import pytest


# ---------------------------------------------------------------------------
# Import hook module (the conftest stubs make this safe)
# ---------------------------------------------------------------------------

import hook


# ---------------------------------------------------------------------------
# Module-level metadata
# ---------------------------------------------------------------------------

class TestHookMetadata:

def test_name(self):
assert hook.name == "Magma"

def test_description_mentions_vue(self):
assert "VueJS" in hook.description

def test_description_mentions_caldera(self):
assert "Caldera" in hook.description

def test_description_is_string(self):
assert isinstance(hook.description, str)

def test_address(self):
assert hook.address == "/plugin/magma/gui"

def test_address_starts_with_slash(self):
assert hook.address.startswith("/")

def test_address_contains_plugin(self):
assert "/plugin/" in hook.address

def test_access_is_app(self):
from app.utility.base_world import BaseWorld
assert hook.access == BaseWorld.Access.APP

def test_access_value(self):
assert hook.access == "app"


# ---------------------------------------------------------------------------
# enable() coroutine
# ---------------------------------------------------------------------------

class TestEnable:

@pytest.mark.asyncio
async def test_enable_is_coroutine(self):
import inspect
assert inspect.iscoroutinefunction(hook.enable)

@pytest.mark.asyncio
async def test_enable_retrieves_app_svc(self, mock_services, mock_app):
await hook.enable(mock_services)
mock_services["app_svc"].application # accessed as attribute

Comment on lines +64 to +68
@pytest.mark.asyncio
async def test_enable_creates_magma_api(self, mock_services, mock_app):
with patch("hook.MagmaAPI") as mock_cls:
await hook.enable(mock_services)
mock_cls.assert_called_once_with(mock_services)

@pytest.mark.asyncio
async def test_enable_passes_services_to_api(self, mock_services, mock_app):
with patch("hook.MagmaAPI") as mock_cls:
await hook.enable(mock_services)
args, _ = mock_cls.call_args
assert args[0] is mock_services

@pytest.mark.asyncio
async def test_enable_accesses_application(self, mock_services, mock_app):
await hook.enable(mock_services)
# The enable function reads app_svc.application
_ = mock_services["app_svc"].application

Comment on lines +83 to +87
@pytest.mark.asyncio
async def test_enable_with_minimal_services(self):
"""enable() needs app_svc at minimum."""
app_svc = MagicMock()
app_svc.application = MagicMock()
services = {"app_svc": app_svc}
# Should not raise
await hook.enable(services)

@pytest.mark.asyncio
async def test_enable_without_app_svc_raises(self):
with pytest.raises((AttributeError, TypeError)):
await hook.enable({})


# ---------------------------------------------------------------------------
# Plugin module attributes expected by Caldera plugin loader
# ---------------------------------------------------------------------------

class TestPluginContract:
"""Caldera expects every plugin hook.py to expose specific module-level
attributes. Verify the contract is satisfied."""

def test_has_name(self):
assert hasattr(hook, "name")

def test_has_description(self):
assert hasattr(hook, "description")

def test_has_address(self):
assert hasattr(hook, "address")

def test_has_access(self):
assert hasattr(hook, "access")

def test_has_enable(self):
assert hasattr(hook, "enable")

def test_enable_is_callable(self):
assert callable(hook.enable)

def test_name_is_non_empty_string(self):
assert isinstance(hook.name, str) and len(hook.name) > 0

def test_description_is_non_empty_string(self):
assert isinstance(hook.description, str) and len(hook.description) > 0

def test_address_is_non_empty_string(self):
assert isinstance(hook.address, str) and len(hook.address) > 0


# ---------------------------------------------------------------------------
# Route / static-file serving expectations
# ---------------------------------------------------------------------------

class TestRouteRegistration:
"""The enable() function should set up the application for serving the
Magma Vue SPA. We test that the expected aiohttp structures are touched."""

@pytest.mark.asyncio
async def test_app_obtained_from_app_svc(self, mock_services, mock_app):
await hook.enable(mock_services)
# Confirm application attribute was accessed
assert mock_services["app_svc"].application is mock_app

@pytest.mark.asyncio
async def test_enable_returns_none(self, mock_services, mock_app):
result = await hook.enable(mock_services)
assert result is None


# ---------------------------------------------------------------------------
# Build / dist expectations
# ---------------------------------------------------------------------------

class TestDistServing:
"""Magma's built Vue frontend lives in a dist/ directory. Verify
assumptions about the address where it would be served."""

def test_gui_address_ends_with_gui(self):
assert hook.address.endswith("/gui")

def test_gui_address_has_magma(self):
assert "magma" in hook.address

def test_gui_address_segments(self):
parts = hook.address.strip("/").split("/")
assert parts == ["plugin", "magma", "gui"]
Loading
Loading