bambuddy/backend/tests/integration/test_external_links_api.py
maziggy 83cbac04b7 - Add interactive API Browser to Settings > API Keys
- New APIBrowser component with full OpenAPI schema integration
    - Fetches and parses /openapi.json automatically
    - Groups endpoints by API tags (printers, archives, settings, etc.)
    - Expandable endpoint sections with color-coded method badges
    - Path parameter, query parameter, and JSON body editors
    - Auto-populates request body with schema examples
    - Live API request execution with response display
    - Response shows status code, timing, and formatted JSON
    - Copy response button with clipboard fallback
    - Search to filter endpoints across all categories
    - Expand All / Collapse All buttons
    - Link to Swagger UI (/docs)

  - Two-column layout for API Keys tab
    - Left: API key management + webhook documentation
    - Right: API Browser with dedicated test key input

  - Parameter validation
    - Shows warning for missing required parameters
    - Validates before sending requests to avoid 422 errors

  - UX improvements
    - "Use in API Browser" button on newly created keys
    - Responsive layout (stacked on mobile, side-by-side on xl+)
2026-01-02 15:00:21 +01:00

169 lines
6.3 KiB
Python

"""Integration tests for External Links API endpoints."""
import pytest
from httpx import AsyncClient
class TestExternalLinksAPI:
"""Integration tests for /api/v1/external-links endpoints."""
@pytest.fixture
async def link_factory(self, db_session):
"""Factory to create test external links."""
_counter = [0]
async def _create_link(**kwargs):
from backend.app.models.external_link import ExternalLink
_counter[0] += 1
counter = _counter[0]
defaults = {
"name": f"Test Link {counter}",
"url": f"https://example.com/{counter}",
"icon": "Link",
"sort_order": counter,
}
defaults.update(kwargs)
link = ExternalLink(**defaults)
db_session.add(link)
await db_session.commit()
await db_session.refresh(link)
return link
return _create_link
@pytest.mark.asyncio
@pytest.mark.integration
async def test_list_external_links_empty(self, async_client: AsyncClient):
"""Verify empty list when no links exist."""
response = await async_client.get("/api/v1/external-links/")
assert response.status_code == 200
assert isinstance(response.json(), list)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_list_external_links_with_data(self, async_client: AsyncClient, link_factory, db_session):
"""Verify list returns existing links."""
await link_factory(name="My Link")
response = await async_client.get("/api/v1/external-links/")
assert response.status_code == 200
data = response.json()
assert any(link["name"] == "My Link" for link in data)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_create_external_link(self, async_client: AsyncClient):
"""Verify external link can be created."""
data = {
"name": "New Link",
"url": "https://new-link.example.com",
"icon": "ExternalLink",
}
response = await async_client.post("/api/v1/external-links/", json=data)
assert response.status_code == 200
result = response.json()
assert result["name"] == "New Link"
assert result["url"] == "https://new-link.example.com"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_external_link(self, async_client: AsyncClient, link_factory, db_session):
"""Verify single link can be retrieved."""
link = await link_factory(name="Get Test Link")
response = await async_client.get(f"/api/v1/external-links/{link.id}")
assert response.status_code == 200
assert response.json()["name"] == "Get Test Link"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_external_link_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent link."""
response = await async_client.get("/api/v1/external-links/9999")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_external_link(self, async_client: AsyncClient, link_factory, db_session):
"""Verify link can be updated."""
link = await link_factory(name="Original")
response = await async_client.patch(
f"/api/v1/external-links/{link.id}", json={"name": "Updated", "url": "https://updated.example.com"}
)
assert response.status_code == 200
result = response.json()
assert result["name"] == "Updated"
assert result["url"] == "https://updated.example.com"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_delete_external_link(self, async_client: AsyncClient, link_factory, db_session):
"""Verify link can be deleted."""
link = await link_factory()
response = await async_client.delete(f"/api/v1/external-links/{link.id}")
assert response.status_code == 200
# Verify deleted
response = await async_client.get(f"/api/v1/external-links/{link.id}")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_reorder_external_links(self, async_client: AsyncClient, link_factory, db_session):
"""Verify links can be reordered."""
link1 = await link_factory(name="Link 1")
link2 = await link_factory(name="Link 2")
link3 = await link_factory(name="Link 3")
# Reorder: 3, 1, 2
response = await async_client.put(
"/api/v1/external-links/reorder", json={"ids": [link3.id, link1.id, link2.id]}
)
assert response.status_code == 200
data = response.json()
# First link should be link3
assert data[0]["id"] == link3.id
assert data[0]["sort_order"] == 0
class TestExternalLinksIconAPI:
"""Tests for external link icon upload/delete."""
@pytest.fixture
async def link_factory(self, db_session):
"""Factory to create test external links."""
async def _create_link(**kwargs):
from backend.app.models.external_link import ExternalLink
defaults = {
"name": "Icon Test Link",
"url": "https://example.com",
"icon": "Link",
"sort_order": 0,
}
defaults.update(kwargs)
link = ExternalLink(**defaults)
db_session.add(link)
await db_session.commit()
await db_session.refresh(link)
return link
return _create_link
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_icon_not_set(self, async_client: AsyncClient, link_factory, db_session):
"""Verify 404 when no custom icon is set."""
link = await link_factory()
response = await async_client.get(f"/api/v1/external-links/{link.id}/icon")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_delete_icon_when_none(self, async_client: AsyncClient, link_factory, db_session):
"""Verify deleting non-existent icon succeeds silently."""
link = await link_factory()
response = await async_client.delete(f"/api/v1/external-links/{link.id}/icon")
assert response.status_code == 200