feat: Split read/write scopes into app:read/write scopes
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Unit tests with mocked dependencies for fast feedback."""
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Unit tests for Pydantic response models."""
|
||||
|
||||
import pytest
|
||||
|
||||
from nextcloud_mcp_server.models.notes import (
|
||||
CreateNoteResponse,
|
||||
Note,
|
||||
NoteSearchResult,
|
||||
SearchNotesResponse,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_note_model_creation():
|
||||
"""Test creating a Note model with required fields."""
|
||||
note = Note(
|
||||
id=123,
|
||||
title="Test Note",
|
||||
content="# Test Content",
|
||||
modified=1700000000,
|
||||
etag="abc123",
|
||||
)
|
||||
|
||||
assert note.id == 123
|
||||
assert note.title == "Test Note"
|
||||
assert note.content == "# Test Content"
|
||||
assert note.category == "" # default value
|
||||
assert note.favorite is False # default value
|
||||
assert note.etag == "abc123"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_note_modified_datetime_property():
|
||||
"""Test that Note.modified_datetime converts Unix timestamp correctly."""
|
||||
note = Note(
|
||||
id=1,
|
||||
title="Test",
|
||||
content="Content",
|
||||
modified=1700000000,
|
||||
etag="etag",
|
||||
)
|
||||
|
||||
dt = note.modified_datetime
|
||||
assert dt.year == 2023 # Nov 14, 2023
|
||||
assert dt.month == 11
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_create_note_response_serialization():
|
||||
"""Test CreateNoteResponse can serialize to JSON."""
|
||||
response = CreateNoteResponse(
|
||||
id=42,
|
||||
title="New Note",
|
||||
category="Work",
|
||||
etag="xyz789",
|
||||
)
|
||||
|
||||
# Test serialization
|
||||
data = response.model_dump()
|
||||
assert data["id"] == 42
|
||||
assert data["title"] == "New Note"
|
||||
assert data["category"] == "Work"
|
||||
assert data["etag"] == "xyz789"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_search_notes_response_wraps_results():
|
||||
"""Test SearchNotesResponse wraps list of results correctly.
|
||||
|
||||
This is critical - FastMCP mangles raw List[Dict] responses,
|
||||
so we must wrap them in a response model.
|
||||
"""
|
||||
results = [
|
||||
NoteSearchResult(id=1, title="First Note", category="Work"),
|
||||
NoteSearchResult(id=2, title="Second Note", category="Personal"),
|
||||
]
|
||||
|
||||
response = SearchNotesResponse(
|
||||
results=results,
|
||||
query="test query",
|
||||
total_found=2,
|
||||
)
|
||||
|
||||
# Verify the response structure
|
||||
assert len(response.results) == 2
|
||||
assert response.results[0].id == 1
|
||||
assert response.results[1].title == "Second Note"
|
||||
assert response.query == "test query"
|
||||
assert response.total_found == 2
|
||||
|
||||
# Verify it serializes correctly
|
||||
data = response.model_dump()
|
||||
assert "results" in data
|
||||
assert isinstance(data["results"], list)
|
||||
assert len(data["results"]) == 2
|
||||
assert data["results"][0]["id"] == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_note_search_result_with_score():
|
||||
"""Test NoteSearchResult with optional score field."""
|
||||
result = NoteSearchResult(
|
||||
id=99,
|
||||
title="Relevant Note",
|
||||
category="Archive",
|
||||
score=0.95,
|
||||
)
|
||||
|
||||
assert result.id == 99
|
||||
assert result.score == 0.95
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_note_search_result_without_score():
|
||||
"""Test NoteSearchResult without optional score field."""
|
||||
result = NoteSearchResult(
|
||||
id=99,
|
||||
title="Relevant Note",
|
||||
category="Archive",
|
||||
)
|
||||
|
||||
assert result.id == 99
|
||||
assert result.score is None
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Unit tests for scope decorator metadata and classification logic."""
|
||||
|
||||
import pytest
|
||||
|
||||
from nextcloud_mcp_server.auth.scope_authorization import (
|
||||
InsufficientScopeError,
|
||||
require_scopes,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_scope_decorator_stores_metadata():
|
||||
"""Test that @require_scopes decorator stores scope requirements as function metadata."""
|
||||
|
||||
@require_scopes("notes:read", "notes:write")
|
||||
async def example_function():
|
||||
pass
|
||||
|
||||
# Verify metadata is stored
|
||||
assert hasattr(example_function, "_required_scopes")
|
||||
assert example_function._required_scopes == ["notes:read", "notes:write"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_scope_decorator_with_single_scope():
|
||||
"""Test decorator with a single scope requirement."""
|
||||
|
||||
@require_scopes("calendar:read")
|
||||
async def example_function():
|
||||
pass
|
||||
|
||||
assert example_function._required_scopes == ["calendar:read"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_scope_decorator_with_no_scopes():
|
||||
"""Test decorator with no scope requirements."""
|
||||
|
||||
@require_scopes()
|
||||
async def example_function():
|
||||
pass
|
||||
|
||||
assert example_function._required_scopes == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_insufficient_scope_error():
|
||||
"""Test InsufficientScopeError exception structure."""
|
||||
missing = ["notes:write", "calendar:write"]
|
||||
error = InsufficientScopeError(missing)
|
||||
|
||||
assert error.missing_scopes == missing
|
||||
assert "notes:write" in str(error)
|
||||
assert "calendar:write" in str(error)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_insufficient_scope_error_with_custom_message():
|
||||
"""Test InsufficientScopeError with custom message."""
|
||||
missing = ["files:write"]
|
||||
custom_msg = "You need more permissions"
|
||||
error = InsufficientScopeError(missing, custom_msg)
|
||||
|
||||
assert error.missing_scopes == missing
|
||||
assert str(error) == custom_msg
|
||||
Reference in New Issue
Block a user