Initial commit: RehearsalHub POC
Full-stack self-hosted band rehearsal platform: Backend (FastAPI + SQLAlchemy 2.0 async): - Auth with JWT (register, login, /me, settings) - Band management with Nextcloud folder integration - Song management with audio version tracking - Nextcloud scan to auto-import audio files - Band membership with link-based invite system - Song comments - Audio analysis worker (BPM, key, loudness, waveform) - Nextcloud activity watcher for auto-import - WebSocket support for real-time annotation updates - Alembic migrations (0001–0003) - Repository pattern, Ruff + mypy configured Frontend (React 18 + Vite + TypeScript strict): - Login/register page with post-login redirect - Home page with band list and creation form - Band page with member panel, invite link, song list, NC scan - Song page with waveform player, annotations, comment thread - Settings page for per-user Nextcloud credentials - Invite acceptance page (/invite/:token) - ESLint v9 flat config + TypeScript strict mode Infrastructure: - Docker Compose: PostgreSQL, Redis, API, worker, watcher, nginx - nginx reverse proxy for static files + /api/ proxy - make check runs all linters before docker compose build Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
watcher/tests/test_nc_client.py
Normal file
80
watcher/tests/test_nc_client.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Tests for Nextcloud OCS client."""
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
import httpx
|
||||
|
||||
from watcher.nc_client import NextcloudWatcherClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return NextcloudWatcherClient(
|
||||
base_url="http://nc.test", username="admin", password="secret"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activities_returns_list(client):
|
||||
mock_response = {
|
||||
"ocs": {
|
||||
"data": [
|
||||
{
|
||||
"activity_id": 1,
|
||||
"subject": "file_created",
|
||||
"objects": {"123": "/bands/myband/songs/song1/take1.wav"},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
with respx.mock:
|
||||
respx.get("http://nc.test/ocs/v2.php/apps/activity/api/v2/activity/files").mock(
|
||||
return_value=httpx.Response(200, json=mock_response)
|
||||
)
|
||||
activities = await client.get_activities(since_id=0)
|
||||
|
||||
assert len(activities) == 1
|
||||
assert activities[0]["subject"] == "file_created"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activities_returns_empty_on_no_data(client):
|
||||
mock_response = {"ocs": {"data": []}}
|
||||
with respx.mock:
|
||||
respx.get("http://nc.test/ocs/v2.php/apps/activity/api/v2/activity/files").mock(
|
||||
return_value=httpx.Response(200, json=mock_response)
|
||||
)
|
||||
activities = await client.get_activities()
|
||||
|
||||
assert activities == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_healthy_true_when_installed(client):
|
||||
with respx.mock:
|
||||
respx.get("http://nc.test/status.php").mock(
|
||||
return_value=httpx.Response(200, json={"installed": True, "version": "28.0.0"})
|
||||
)
|
||||
result = await client.is_healthy()
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_healthy_false_when_not_installed(client):
|
||||
with respx.mock:
|
||||
respx.get("http://nc.test/status.php").mock(
|
||||
return_value=httpx.Response(200, json={"installed": False})
|
||||
)
|
||||
result = await client.is_healthy()
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_healthy_false_on_connection_error(client):
|
||||
with respx.mock:
|
||||
respx.get("http://nc.test/status.php").mock(side_effect=httpx.ConnectError("refused"))
|
||||
result = await client.is_healthy()
|
||||
|
||||
assert result is False
|
||||
Reference in New Issue
Block a user