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
153 changes: 153 additions & 0 deletions fastapi_startkit/tests/broadcasting/test_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from types import SimpleNamespace

from fastapi_startkit.application import Application
from fastapi_startkit.broadcasting.manager import BroadcastManager
from fastapi_startkit.broadcasting.provider import ReverbProvider
from fastapi_startkit.broadcasting.reverb.server import ReverbServer


_MISSING = object()


def make_app(tmp_path):
return Application(base_path=tmp_path, env="testing", providers=[ReverbProvider])


def mount_auth(tmp_path):
"""Register broadcasting and mount only the auth endpoint.

The full ``boot()`` mounts a catch-all WebSocket sub-app at ``""`` which
shadows the HTTP auth route, so exercise the auth handler on its own.
"""
app = Application(base_path=tmp_path, env="testing", providers=[])
provider = ReverbProvider(app)
provider.register()
provider._mount_auth_endpoint()
return app


def auth_handler(app):
for route in app.fastapi.routes:
if getattr(route, "path", None) == "/broadcasting/auth":
return route.endpoint
raise AssertionError("auth route not mounted")


class FakeRequest:
"""Stand-in for a Starlette Request.

``from __future__ import annotations`` in the provider turns the handler's
``Request`` annotation into an unresolved forward-ref, so FastAPI cannot
inject a real request through the test client. Calling the handler with
this fake exercises the same body directly.
"""

def __init__(self, content_type, *, json_body=None, form_body=None, user=_MISSING):
self.headers = {"content-type": content_type}
self._json = json_body or {}
self._form = form_body or {}
self.state = SimpleNamespace()
if user is not _MISSING:
self.state.user = user

async def json(self):
return self._json

async def form(self):
return self._form


class TestRegister:
def test_binds_manager_and_server(self, tmp_path):
app = make_app(tmp_path)
assert isinstance(app.make("broadcasting"), BroadcastManager)
assert isinstance(app.make("reverb.server"), ReverbServer)

def test_publishes_channels_stub(self, tmp_path):
app = make_app(tmp_path)
published = app.published_resources.get("broadcasting", {})
assert any(dest == "routes/channels.py" for dest in published.values())


class TestAuthEndpoint:
async def test_public_channel_allowed_json(self, tmp_path):
handler = auth_handler(mount_auth(tmp_path))
response = await handler(FakeRequest("application/json", json_body={"channel_name": "orders"}))
assert response.status_code == 200

async def test_private_channel_denied_without_rule(self, tmp_path):
handler = auth_handler(mount_auth(tmp_path))
response = await handler(FakeRequest("application/json", json_body={"channel_name": "private-secret"}))
assert response.status_code == 403

async def test_accepts_form_encoded_body(self, tmp_path):
handler = auth_handler(mount_auth(tmp_path))
response = await handler(
FakeRequest("application/x-www-form-urlencoded", form_body={"channel_name": "public-room"})
)
assert response.status_code == 200

async def test_uses_request_state_user(self, tmp_path):
handler = auth_handler(mount_auth(tmp_path))
response = await handler(FakeRequest("application/json", json_body={"channel_name": "orders"}, user=object()))
assert response.status_code == 200

async def test_resolves_user_from_callable_auth_service(self, tmp_path):
app = mount_auth(tmp_path)
app.bind("auth", SimpleNamespace(user=lambda: SimpleNamespace(id=1)))
handler = auth_handler(app)
response = await handler(FakeRequest("application/json", json_body={"channel_name": "orders"}))
assert response.status_code == 200

async def test_resolves_user_from_attribute_auth_service(self, tmp_path):
app = mount_auth(tmp_path)
app.bind("auth", SimpleNamespace(user=SimpleNamespace(id=2)))
handler = auth_handler(app)
response = await handler(FakeRequest("application/json", json_body={"channel_name": "orders"}))
assert response.status_code == 200


class TestChannelsFileAutoloading:
async def test_registered_channel_authorizer_is_used(self, tmp_path):
routes = tmp_path / "routes"
routes.mkdir()
(routes / "channels.py").write_text(
"from fastapi_startkit.broadcasting import channel\n"
"\n"
"@channel('private-room.{room}')\n"
"async def authorize_room(user, room):\n"
" return True\n"
)

app = make_app(tmp_path)
registry = app.make("broadcasting").channel_registry
assert await registry.authorize("private-room.7", None) is True

def test_missing_channels_file_is_ignored(self, tmp_path):
# No routes/channels.py — boot must still succeed.
app = make_app(tmp_path)
assert isinstance(app.make("broadcasting"), BroadcastManager)


class _RaisingFastapiApp:
"""Minimal app whose ``.fastapi`` raises, mimicking FastAPI not installed."""

base_path = "/nonexistent-base-path"

@property
def fastapi(self):
raise RuntimeError("FastAPI not available")


class TestMountingWithoutFastapi:
def test_mount_websocket_skips_when_fastapi_unavailable(self):
provider = ReverbProvider(_RaisingFastapiApp())
provider._mount_websocket() # returns without raising

def test_mount_auth_endpoint_skips_when_fastapi_unavailable(self):
provider = ReverbProvider(_RaisingFastapiApp())
provider._mount_auth_endpoint() # returns without raising

def test_load_channels_file_skips_when_absent(self):
provider = ReverbProvider(_RaisingFastapiApp())
provider._load_channels_file() # base_path has no routes/channels.py
58 changes: 58 additions & 0 deletions fastapi_startkit/tests/inertia/test_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import json
from types import SimpleNamespace
from unittest.mock import MagicMock

from fastapi_startkit.inertia.inertia import Inertia
from fastapi_startkit.inertia.middleware import InertiaMiddleware
from fastapi_startkit.inertia.provider import InertiaProvider


def make_templates():
"""A stand-in for Jinja2Templates exposing an ``env.globals`` dict."""
return SimpleNamespace(env=SimpleNamespace(globals={}))


class TestRegister:
def test_binds_inertia(self):
app = MagicMock()
InertiaProvider(app).register()
app.bind.assert_called_once_with("inertia", Inertia)


class TestBoot:
def test_adds_middleware_and_injects_globals_when_templates_present(self):
templates = make_templates()
app = MagicMock()
app.has.return_value = True
app.make.side_effect = lambda key: {"templates": templates, "inertia": "INERTIA"}[key]

InertiaProvider(app).boot()

app.add_middleware.assert_called_once_with(InertiaMiddleware)
assert callable(templates.env.globals["inertia"])
assert templates.env.globals["Inertia"] == "INERTIA"

def test_skips_globals_when_templates_absent(self):
app = MagicMock()
app.has.return_value = False

InertiaProvider(app).boot()

app.add_middleware.assert_called_once_with(InertiaMiddleware)
app.make.assert_not_called()

def test_inertia_helper_renders_page_markup(self):
templates = make_templates()
app = MagicMock()
app.has.return_value = True
app.make.side_effect = lambda key: {"templates": templates, "inertia": "INERTIA"}[key]

InertiaProvider(app).boot()
helper = templates.env.globals["inertia"]

page = {"component": "Dashboard", "props": {"count": 3}}
html = str(helper(page))

assert json.dumps(page) in html
assert 'data-page="app"' in html
assert 'id="app"' in html
61 changes: 61 additions & 0 deletions fastapi_startkit/tests/storage/test_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest.mock import MagicMock

from fastapi.testclient import TestClient

from fastapi_startkit.application import Application
from fastapi_startkit.storage.providers.provider import StorageProvider
from fastapi_startkit.storage.storage import StorageManager


def make_app(tmp_path):
return Application(base_path=tmp_path, env="testing", providers=[StorageProvider])


class TestRegister:
def test_binds_storage_manager_with_drivers(self, tmp_path):
app = make_app(tmp_path)
storage = app.make("storage")
assert isinstance(storage, StorageManager)
assert "local" in storage.drivers
assert "s3" in storage.drivers


class TestBootRoute:
def test_serves_existing_public_file(self, tmp_path):
app = make_app(tmp_path)
public_dir = tmp_path / "storage" / "app" / "public"
(public_dir / "hello.txt").write_text("hi there")

client = TestClient(app.fastapi)
response = client.get("/storage/hello.txt")

assert response.status_code == 200
assert response.text == "hi there"

def test_returns_404_for_missing_file(self, tmp_path):
app = make_app(tmp_path)
client = TestClient(app.fastapi)
assert client.get("/storage/nope.txt").status_code == 404

def test_returns_404_for_directory_path(self, tmp_path):
app = make_app(tmp_path)
public_dir = tmp_path / "storage" / "app" / "public"
(public_dir / "subdir").mkdir(parents=True, exist_ok=True)

client = TestClient(app.fastapi)
assert client.get("/storage/subdir").status_code == 404


class TestBootWithoutFastapi:
def test_boot_returns_early_when_no_fastapi(self):
app = MagicMock()
app.published_resources = {}
app.fastapi = None

provider = StorageProvider(app)
# Returns early on the falsy fastapi check — no route registration,
# no mkdir. Reaching here without error exercises that branch.
provider.boot()

# The config stub is still published before the early return.
assert app.published_resources != {}
Loading
Loading