test: Remove unused pytest fixtures

This commit is contained in:
Chris Coutinho
2025-10-14 01:23:39 +02:00
parent f48d3714d2
commit 13e4915e38
19 changed files with 115 additions and 233 deletions
+1 -1
View File
@@ -62,4 +62,4 @@ jobs:
NEXTCLOUD_USERNAME: "admin" NEXTCLOUD_USERNAME: "admin"
NEXTCLOUD_PASSWORD: "admin" NEXTCLOUD_PASSWORD: "admin"
run: | run: |
uv run pytest -v --browser firefox -k oauth uv run pytest -v --browser firefox
+8 -13
View File
@@ -1,35 +1,30 @@
import click
import logging import logging
import os import os
import uvicorn
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from contextlib import asynccontextmanager, AsyncExitStack from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass from dataclasses import dataclass
import click
import uvicorn
from mcp.server.auth.settings import AuthSettings
from mcp.server.fastmcp import Context, FastMCP
from pydantic import AnyHttpUrl from pydantic import AnyHttpUrl
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.routing import Mount from starlette.routing import Mount
from mcp.server.fastmcp import Context, FastMCP from nextcloud_mcp_server.auth import NextcloudTokenVerifier, load_or_register_client
from mcp.server.auth.settings import AuthSettings
from nextcloud_mcp_server.config import setup_logging
from nextcloud_mcp_server.client import NextcloudClient from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.config import setup_logging
from nextcloud_mcp_server.context import get_client as get_nextcloud_client from nextcloud_mcp_server.context import get_client as get_nextcloud_client
from nextcloud_mcp_server.auth import (
NextcloudTokenVerifier,
load_or_register_client,
)
from nextcloud_mcp_server.server import ( from nextcloud_mcp_server.server import (
configure_calendar_tools, configure_calendar_tools,
configure_contacts_tools, configure_contacts_tools,
configure_deck_tools,
configure_notes_tools, configure_notes_tools,
configure_tables_tools, configure_tables_tools,
configure_webdav_tools, configure_webdav_tools,
configure_deck_tools,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1 -1
View File
@@ -2,8 +2,8 @@
import logging import logging
from mcp.server.fastmcp import Context
from mcp.server.auth.provider import AccessToken from mcp.server.auth.provider import AccessToken
from mcp.server.fastmcp import Context
from ..client import NextcloudClient from ..client import NextcloudClient
+2 -2
View File
@@ -2,13 +2,13 @@ import logging
import os import os
from httpx import ( from httpx import (
AsyncBaseTransport,
AsyncClient, AsyncClient,
AsyncHTTPTransport,
Auth, Auth,
BasicAuth, BasicAuth,
Request, Request,
Response, Response,
AsyncBaseTransport,
AsyncHTTPTransport,
) )
from ..controllers.notes_search import NotesSearchController from ..controllers.notes_search import NotesSearchController
+4 -4
View File
@@ -1,11 +1,11 @@
"""Base client for Nextcloud operations with shared authentication.""" """Base client for Nextcloud operations with shared authentication."""
import logging import logging
from abc import ABC
from functools import wraps
import time import time
from httpx import HTTPStatusError, codes, RequestError, AsyncClient from abc import ABC
from functools import wraps
from httpx import AsyncClient, HTTPStatusError, RequestError, codes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+3 -1
View File
@@ -1,10 +1,12 @@
"""CardDAV client for NextCloud contacts operations.""" """CardDAV client for NextCloud contacts operations."""
import logging import logging
from .base import BaseNextcloudClient
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from pythonvCard4.vcard import Contact from pythonvCard4.vcard import Contact
from .base import BaseNextcloudClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+6 -6
View File
@@ -1,16 +1,16 @@
from typing import List, Optional, Dict, Any from typing import Any, Dict, List, Optional
from nextcloud_mcp_server.client.base import BaseNextcloudClient from nextcloud_mcp_server.client.base import BaseNextcloudClient
from nextcloud_mcp_server.models.deck import ( from nextcloud_mcp_server.models.deck import (
DeckBoard,
DeckStack,
DeckCard,
DeckLabel,
DeckACL, DeckACL,
DeckAttachment, DeckAttachment,
DeckBoard,
DeckCard,
DeckComment, DeckComment,
DeckSession,
DeckConfig, DeckConfig,
DeckLabel,
DeckSession,
DeckStack,
) )
+36 -40
View File
@@ -1,41 +1,25 @@
"""Pydantic models for structured MCP server responses.""" """Pydantic models for structured MCP server responses."""
# Base models # Base models
from .base import ( from .base import BaseResponse, IdResponse, StatusResponse
BaseResponse,
IdResponse,
StatusResponse,
)
# Notes models
from .notes import (
Note,
NoteSearchResult,
NotesSettings,
CreateNoteResponse,
UpdateNoteResponse,
DeleteNoteResponse,
AppendContentResponse,
SearchNotesResponse,
)
# Calendar models # Calendar models
from .calendar import ( from .calendar import (
AvailabilitySlot,
BulkOperationResponse,
BulkOperationResult,
Calendar, Calendar,
CalendarEvent, CalendarEvent,
CalendarEventSummary, CalendarEventSummary,
CreateEventResponse, CreateEventResponse,
UpdateEventResponse,
DeleteEventResponse,
ListEventsResponse,
ListCalendarsResponse,
AvailabilitySlot,
FindAvailabilityResponse,
BulkOperationResult,
BulkOperationResponse,
CreateMeetingResponse, CreateMeetingResponse,
UpcomingEventsResponse, DeleteEventResponse,
FindAvailabilityResponse,
ListCalendarsResponse,
ListEventsResponse,
ManageCalendarResponse, ManageCalendarResponse,
UpcomingEventsResponse,
UpdateEventResponse,
) )
# Contacts models # Contacts models
@@ -43,38 +27,50 @@ from .contacts import (
AddressBook, AddressBook,
Contact, Contact,
ContactField, ContactField,
CreateAddressBookResponse,
CreateContactResponse,
DeleteAddressBookResponse,
DeleteContactResponse,
ListAddressBooksResponse, ListAddressBooksResponse,
ListContactsResponse, ListContactsResponse,
CreateContactResponse,
UpdateContactResponse, UpdateContactResponse,
DeleteContactResponse, )
CreateAddressBookResponse,
DeleteAddressBookResponse, # Notes models
from .notes import (
AppendContentResponse,
CreateNoteResponse,
DeleteNoteResponse,
Note,
NoteSearchResult,
NotesSettings,
SearchNotesResponse,
UpdateNoteResponse,
) )
# Tables models # Tables models
from .tables import ( from .tables import (
CreateRowResponse,
DeleteRowResponse,
GetSchemaResponse,
ListTablesResponse,
ReadTableResponse,
Table, Table,
TableColumn, TableColumn,
TableRow, TableRow,
TableView,
TableSchema, TableSchema,
ListTablesResponse, TableView,
GetSchemaResponse,
ReadTableResponse,
CreateRowResponse,
UpdateRowResponse, UpdateRowResponse,
DeleteRowResponse,
) )
# WebDAV models # WebDAV models
from .webdav import ( from .webdav import (
FileInfo,
DirectoryListing,
ReadFileResponse,
WriteFileResponse,
CreateDirectoryResponse, CreateDirectoryResponse,
DeleteResourceResponse, DeleteResourceResponse,
DirectoryListing,
FileInfo,
ReadFileResponse,
WriteFileResponse,
) )
__all__ = [ __all__ = [
+1 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional, Dict, Any, Union from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
+2 -2
View File
@@ -1,9 +1,9 @@
from .calendar import configure_calendar_tools from .calendar import configure_calendar_tools
from .contacts import configure_contacts_tools
from .deck import configure_deck_tools
from .notes import configure_notes_tools from .notes import configure_notes_tools
from .tables import configure_tables_tools from .tables import configure_tables_tools
from .webdav import configure_webdav_tools from .webdav import configure_webdav_tools
from .contacts import configure_contacts_tools
from .deck import configure_deck_tools
__all__ = [ __all__ = [
"configure_calendar_tools", "configure_calendar_tools",
+1 -4
View File
@@ -5,10 +5,7 @@ from typing import Optional
from mcp.server.fastmcp import Context, FastMCP from mcp.server.fastmcp import Context, FastMCP
from nextcloud_mcp_server.context import get_client from nextcloud_mcp_server.context import get_client
from nextcloud_mcp_server.models.calendar import ( from nextcloud_mcp_server.models.calendar import Calendar, ListCalendarsResponse
Calendar,
ListCalendarsResponse,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+7 -7
View File
@@ -5,17 +5,17 @@ from mcp.server.fastmcp import Context, FastMCP
from nextcloud_mcp_server.context import get_client from nextcloud_mcp_server.context import get_client
from nextcloud_mcp_server.models.deck import ( from nextcloud_mcp_server.models.deck import (
CardOperationResponse,
CreateBoardResponse,
CreateCardResponse,
CreateLabelResponse,
CreateStackResponse,
DeckBoard, DeckBoard,
DeckStack,
DeckCard, DeckCard,
DeckLabel, DeckLabel,
CreateBoardResponse, DeckStack,
CreateStackResponse,
StackOperationResponse,
CreateCardResponse,
CardOperationResponse,
CreateLabelResponse,
LabelOperationResponse, LabelOperationResponse,
StackOperationResponse,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+8 -8
View File
@@ -1,20 +1,20 @@
import logging import logging
from httpx import HTTPStatusError from httpx import HTTPStatusError
from mcp.server.fastmcp import Context, FastMCP
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import McpError
from mcp.types import ErrorData from mcp.types import ErrorData
from mcp.server.fastmcp import Context, FastMCP
from nextcloud_mcp_server.context import get_client from nextcloud_mcp_server.context import get_client
from nextcloud_mcp_server.models.notes import ( from nextcloud_mcp_server.models.notes import (
Note,
NotesSettings,
CreateNoteResponse,
UpdateNoteResponse,
DeleteNoteResponse,
AppendContentResponse, AppendContentResponse,
SearchNotesResponse, CreateNoteResponse,
DeleteNoteResponse,
Note,
NoteSearchResult, NoteSearchResult,
NotesSettings,
SearchNotesResponse,
UpdateNoteResponse,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+8 -110
View File
@@ -572,109 +572,6 @@ async def temporary_board_with_card(
logger.error(f"Unexpected error deleting temporary card {card.id}: {e}") logger.error(f"Unexpected error deleting temporary card {card.id}: {e}")
async def get_oauth_token(nextcloud_url: str, username: str, password: str) -> str:
"""
Get an OAuth access token from Nextcloud OIDC using Client Credentials flow.
This is a helper function for testing only - it bypasses the normal OAuth flow
to directly obtain a token for automated testing.
Args:
nextcloud_url: Nextcloud base URL
username: Nextcloud username
password: Nextcloud password
Returns:
Access token string
Raises:
Exception: If token acquisition fails
"""
from nextcloud_mcp_server.auth.client_registration import load_or_register_client
logger.info(f"Getting OAuth token for testing from {nextcloud_url}")
# Perform OIDC discovery
async with httpx.AsyncClient() as http_client:
discovery_url = f"{nextcloud_url}/.well-known/openid-configuration"
logger.debug(f"Fetching OIDC discovery from: {discovery_url}")
discovery_response = await http_client.get(discovery_url)
if discovery_response.status_code != 200:
raise Exception(f"OIDC discovery failed: {discovery_response.status_code}")
oidc_config = discovery_response.json()
token_endpoint = oidc_config.get("token_endpoint")
registration_endpoint = oidc_config.get("registration_endpoint")
if not token_endpoint or not registration_endpoint:
raise Exception("OIDC discovery missing required endpoints")
logger.debug(f"Token endpoint: {token_endpoint}")
logger.debug(f"Registration endpoint: {registration_endpoint}")
# Get or register an OAuth client
client_info = await load_or_register_client(
nextcloud_url=nextcloud_url,
registration_endpoint=registration_endpoint,
storage_path=".nextcloud_oauth_test_client.json",
redirect_uris=["http://localhost:8000/oauth/callback"],
)
# Use client credentials to get a token via client_credentials grant
token_response = await http_client.post(
token_endpoint,
data={
"grant_type": "client_credentials",
"client_id": client_info.client_id,
"client_secret": client_info.client_secret,
"scope": "openid profile email",
},
)
if token_response.status_code != 200:
logger.error(f"Failed to get OAuth token: {token_response.text}")
raise Exception(f"Token request failed: {token_response.status_code}")
token_data = token_response.json()
access_token = token_data.get("access_token")
if not access_token:
raise Exception("No access_token in response")
logger.info("Successfully obtained OAuth access token for testing")
return access_token
@pytest.fixture(scope="session")
async def oauth_token() -> str:
"""
Fixture to obtain an OAuth access token for integration tests.
This uses the Resource Owner Password flow to get a token without
requiring interactive browser authentication.
"""
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME")
password = os.getenv("NEXTCLOUD_PASSWORD")
if not all([nextcloud_host, username, password]):
pytest.skip(
"OAuth token fixture requires NEXTCLOUD_HOST, USERNAME, and PASSWORD"
)
# Wait for Nextcloud to be ready
if not await wait_for_nextcloud(nextcloud_host):
pytest.fail(f"Nextcloud server at {nextcloud_host} is not ready")
try:
token = await get_oauth_token(nextcloud_host, username, password)
return token
except Exception as e:
logger.error(f"Failed to obtain OAuth token: {e}")
pytest.skip(f"Could not obtain OAuth token for testing: {e}")
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def nc_oauth_client_interactive( async def nc_oauth_client_interactive(
interactive_oauth_token: str, interactive_oauth_token: str,
@@ -771,9 +668,9 @@ def oauth_callback_server():
# Skip interactive tests in CI environments # Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"): if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI") pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
from http.server import BaseHTTPRequestHandler, HTTPServer
import threading import threading
from urllib.parse import urlparse, parse_qs from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
# Use a mutable container to share state across threads # Use a mutable container to share state across threads
auth_state = {"code": None} auth_state = {"code": None}
@@ -843,6 +740,10 @@ def oauth_callback_server():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@pytest.mark.skipif(
"GITHUB_ACTIONS" in os.environ,
reason="Unable to access interactive browser in GitHub Actions",
)
async def interactive_oauth_token(oauth_callback_server) -> str: async def interactive_oauth_token(oauth_callback_server) -> str:
""" """
Fixture to obtain an OAuth access token for integration tests. Fixture to obtain an OAuth access token for integration tests.
@@ -852,12 +753,9 @@ async def interactive_oauth_token(oauth_callback_server) -> str:
Automatically skips when running in GitHub Actions CI. Automatically skips when running in GitHub Actions CI.
""" """
# Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
import webbrowser
import time import time
import webbrowser
from nextcloud_mcp_server.auth.client_registration import load_or_register_client from nextcloud_mcp_server.auth.client_registration import load_or_register_client
@@ -948,7 +846,7 @@ async def playwright_oauth_token(browser) -> str:
- Browser fixture provided by pytest-playwright-asyncio - Browser fixture provided by pytest-playwright-asyncio
- See: https://playwright.dev/python/docs/test-runners - See: https://playwright.dev/python/docs/test-runners
""" """
from urllib.parse import urlparse, parse_qs from urllib.parse import parse_qs, urlparse
nextcloud_host = os.getenv("NEXTCLOUD_HOST") nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME") username = os.getenv("NEXTCLOUD_USERNAME")
+1 -1
View File
@@ -5,7 +5,7 @@ import pytest
from httpx import HTTPStatusError from httpx import HTTPStatusError
from nextcloud_mcp_server.client import NextcloudClient from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.models.deck import DeckStack, DeckCard, DeckLabel from nextcloud_mcp_server.models.deck import DeckCard, DeckLabel, DeckStack
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
+1 -1
View File
@@ -1,9 +1,9 @@
"""Test error propagation in the MCP server for various error scenarios.""" """Test error propagation in the MCP server for various error scenarios."""
import logging import logging
from mcp import ClientSession
import pytest import pytest
from mcp import ClientSession
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+2 -1
View File
@@ -5,10 +5,11 @@ are present in calendar events and contacts during round-trip operations.
""" """
import logging import logging
import pytest
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+22 -29
View File
@@ -9,35 +9,28 @@ logger = logging.getLogger(__name__)
pytestmark = [pytest.mark.integration, pytest.mark.oauth] pytestmark = [pytest.mark.integration, pytest.mark.oauth]
class TestOAuthInteractive: async def test_oauth_client_with_interactive_flow(nc_oauth_client_interactive):
"""Test interactive OAuth authentication with manual browser login.""" """Test that OAuth client created via interactive flow can access Nextcloud APIs."""
# Test 1: Check capabilities
capabilities = await nc_oauth_client_interactive.capabilities()
assert capabilities is not None
logger.info("OAuth client (interactive) successfully fetched capabilities")
async def test_oauth_client_with_interactive_flow( # Test 2: List notes
self, nc_oauth_client_interactive notes = await nc_oauth_client_interactive.notes.get_all_notes()
): assert isinstance(notes, list)
"""Test that OAuth client created via interactive flow can access Nextcloud APIs.""" logger.info(f"OAuth client (interactive) successfully listed {len(notes)} notes")
# Test 1: Check capabilities
capabilities = await nc_oauth_client_interactive.capabilities()
assert capabilities is not None
logger.info("OAuth client (interactive) successfully fetched capabilities")
# Test 2: List notes # Test 3: Create and delete a note
notes = await nc_oauth_client_interactive.notes.get_all_notes() test_note = await nc_oauth_client_interactive.notes.create_note(
assert isinstance(notes, list) title="OAuth Interactive Test Note",
logger.info( content="This note was created during OAuth interactive testing",
f"OAuth client (interactive) successfully listed {len(notes)} notes" )
) assert test_note is not None
assert test_note.get("id") is not None
note_id = test_note["id"]
logger.info(f"OAuth client (interactive) successfully created note {note_id}")
# Test 3: Create and delete a note # Clean up
test_note = await nc_oauth_client_interactive.notes.create_note( await nc_oauth_client_interactive.notes.delete_note(note_id=note_id)
title="OAuth Interactive Test Note", logger.info(f"OAuth client (interactive) successfully deleted note {note_id}")
content="This note was created during OAuth interactive testing",
)
assert test_note is not None
assert test_note.get("id") is not None
note_id = test_note["id"]
logger.info(f"OAuth client (interactive) successfully created note {note_id}")
# Clean up
await nc_oauth_client_interactive.notes.delete_note(note_id=note_id)
logger.info(f"OAuth client (interactive) successfully deleted note {note_id}")
+1 -1
View File
@@ -1,9 +1,9 @@
"""Unit tests for Pydantic models and serialization.""" """Unit tests for Pydantic models and serialization."""
from datetime import datetime, timezone
import json import json
import logging import logging
import re import re
from datetime import datetime, timezone
from nextcloud_mcp_server.models.base import BaseResponse from nextcloud_mcp_server.models.base import BaseResponse