refactor: convert f-string logging to lazy %-style format (G004)

Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.

Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.

Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
  (printf-style specs aren't 1:1 with Python format specs, so we
  delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved

Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-13 01:12:17 +02:00
co-authored by Claude Opus 4.7
parent a4e6125d28
commit 665cb9b1eb
112 changed files with 2534 additions and 1859 deletions
+42 -36
View File
@@ -81,7 +81,7 @@ async def login_flow_oauth_client_credentials(anyio_backend, oauth_callback_serv
token_type="Bearer",
)
logger.info(f"Login Flow OAuth client ready: {client_info.client_id[:16]}...")
logger.info("Login Flow OAuth client ready: %s...", client_info.client_id[:16])
yield (
client_info.client_id,
@@ -101,10 +101,10 @@ async def login_flow_oauth_client_credentials(anyio_backend, oauth_callback_serv
registration_client_uri=client_info.registration_client_uri,
)
logger.info(
f"Cleaned up Login Flow OAuth client: {client_info.client_id[:16]}..."
"Cleaned up Login Flow OAuth client: %s...", client_info.client_id[:16]
)
except Exception as e:
logger.warning(f"Failed to clean up Login Flow OAuth client: {e}")
logger.warning("Failed to clean up Login Flow OAuth client: %s", e)
@pytest.fixture(scope="session")
@@ -137,7 +137,7 @@ async def login_flow_oauth_token(
)
resource_id = resource_metadata.get("resource")
except Exception as e:
logger.warning(f"Failed to fetch resource metadata from port 8004: {e}")
logger.warning("Failed to fetch resource metadata from port 8004: %s", e)
resource_id = None
state = secrets.token_urlsafe(32)
@@ -242,9 +242,9 @@ async def _complete_login_flow_v2(browser, login_url: str) -> None:
page = await context.new_page()
try:
logger.info(f"Opening Login Flow v2 URL: {login_url[:80]}...")
logger.info("Opening Login Flow v2 URL: %s...", login_url[:80])
await page.goto(login_url, wait_until="networkidle", timeout=60000)
logger.info(f"Step 1 - Current URL: {page.url}")
logger.info("Step 1 - Current URL: %s", page.url)
# Step 1: "Connect to your account" page - click "Log in"
login_btn = page.get_by_role("button", name="Log in")
@@ -256,7 +256,7 @@ async def _complete_login_flow_v2(browser, login_url: str) -> None:
except Exception:
logger.info("No 'Log in' button - may already be on login/grant page")
logger.info(f"Step 2 - Current URL: {page.url}")
logger.info("Step 2 - Current URL: %s", page.url)
# Step 2: Login form (only if not already logged in)
# If the user has an active session, they skip straight to the grant page.
@@ -267,7 +267,7 @@ async def _complete_login_flow_v2(browser, login_url: str) -> None:
await page.locator('input[name="password"]').fill(password)
await page.get_by_role("button", name="Log in", exact=True).click()
await page.wait_for_load_state("networkidle", timeout=60000)
logger.info(f"After login: {page.url}")
logger.info("After login: %s", page.url)
else:
logger.info("No login form - already logged in via session")
@@ -278,7 +278,7 @@ async def _complete_login_flow_v2(browser, login_url: str) -> None:
await grant_btn.click()
logger.info("Clicked 'Grant access'")
except Exception as e:
logger.warning(f"No Grant access button: {e}")
logger.warning("No Grant access button: %s", e)
await page.screenshot(path="/tmp/login_flow_no_grant.png")
# Step 4: Password confirmation dialog
@@ -310,7 +310,7 @@ async def _complete_login_flow_v2(browser, login_url: str) -> None:
except Exception:
# The grant may have completed without the success page being visible
await page.wait_for_load_state("networkidle", timeout=10000)
logger.info(f"Login Flow v2 done. Final URL: {page.url}")
logger.info("Login Flow v2 done. Final URL: %s", page.url)
finally:
await context.close()
@@ -347,14 +347,14 @@ async def nc_mcp_login_flow_client(
completes the Login Flow v2 browser login.
"""
message = params.message
logger.info(f"Elicitation received: {message[:100]}...")
logger.info("Elicitation received: %s...", message[:100])
# Extract login URL from elicitation message
for line in message.split("\n"):
stripped = line.strip()
if stripped.startswith("http") and "/login/v2/" in stripped:
login_url_holder["url"] = stripped
logger.info(f"Extracted login URL: {stripped[:80]}...")
logger.info("Extracted login URL: %s...", stripped[:80])
break
if "url" in login_url_holder:
@@ -381,7 +381,7 @@ async def nc_mcp_login_flow_client(
)
provision_data = json.loads(provision_result.content[0].text)
logger.info(f"Provision result: {provision_data.get('status')}")
logger.info("Provision result: %s", provision_data.get("status"))
# If elicitation didn't fire (client doesn't support it),
# extract URL from the response and complete flow manually
@@ -398,11 +398,12 @@ async def nc_mcp_login_flow_client(
status_result = await session.call_tool("nc_auth_check_status", {})
status_data = json.loads(status_result.content[0].text)
status = status_data.get("status")
logger.info(f"Status check {attempt + 1}/{max_attempts}: {status}")
logger.info("Status check %s/%s: %s", attempt + 1, max_attempts, status)
if status == "provisioned":
logger.info(
f"Login Flow v2 provisioned! Username: {status_data.get('username')}"
"Login Flow v2 provisioned! Username: %s",
status_data.get("username"),
)
break
@@ -698,8 +699,10 @@ async def all_login_flow_user_tokens(
elapsed = time.time() - start_time
logger.info(
f"Fetched {len(results)} login-flow tokens in {elapsed:.1f}s "
f"(~{elapsed / len(results):.1f}s per user)"
"Fetched %s login-flow tokens in %ss (~%ss per user)",
len(results),
format(elapsed, ".1f"),
format(elapsed / len(results), ".1f"),
)
return results # type: ignore[return-value]
@@ -756,8 +759,9 @@ async def _provision_login_flow_mcp_client(
status_data = json.loads(status_result.content[0].text)
if status_data.get("status") == "provisioned":
logger.info(
f"Login Flow v2 provisioned for {username}: "
f"{status_data.get('username')}"
"Login Flow v2 provisioned for %s: %s",
username,
status_data.get("username"),
)
break
if status_data.get("status") in ("not_initiated", "error"):
@@ -795,44 +799,44 @@ async def _complete_login_flow_v2_as_user(
page = await context.new_page()
try:
logger.info(f"[{username}] Opening Login Flow v2 URL: {login_url[:80]}...")
logger.info("[%s] Opening Login Flow v2 URL: %s...", username, login_url[:80])
await page.goto(login_url, wait_until="networkidle", timeout=60000)
logger.info(f"[{username}] Step 1 - Current URL: {page.url}")
logger.info("[%s] Step 1 - Current URL: %s", username, page.url)
# Step 1: "Connect to your account" page - click "Log in"
login_btn = page.get_by_role("button", name="Log in")
try:
await login_btn.wait_for(timeout=10000)
await login_btn.click()
logger.info(f"[{username}] Clicked 'Log in' on Connect page")
logger.info("[%s] Clicked 'Log in' on Connect page", username)
await page.wait_for_load_state("networkidle", timeout=30000)
except Exception:
logger.info(
f"[{username}] No 'Log in' button - may already be on login/grant page"
"[%s] No 'Log in' button - may already be on login/grant page", username
)
logger.info(f"[{username}] Step 2 - Current URL: {page.url}")
logger.info("[%s] Step 2 - Current URL: %s", username, page.url)
# Step 2: Login form (only if not already logged in)
user_field = page.locator('input[name="user"]')
if await user_field.count() > 0:
logger.info(f"[{username}] Login form detected, filling credentials...")
logger.info("[%s] Login form detected, filling credentials...", username)
await user_field.fill(username)
await page.locator('input[name="password"]').fill(password)
await page.get_by_role("button", name="Log in", exact=True).click()
await page.wait_for_load_state("networkidle", timeout=60000)
logger.info(f"[{username}] After login: {page.url}")
logger.info("[%s] After login: %s", username, page.url)
else:
logger.info(f"[{username}] No login form - already logged in via session")
logger.info("[%s] No login form - already logged in via session", username)
# Step 3: "Account access" grant page - click "Grant access"
grant_btn = page.get_by_role("button", name="Grant access")
try:
await grant_btn.wait_for(timeout=15000)
await grant_btn.click()
logger.info(f"[{username}] Clicked 'Grant access'")
logger.info("[%s] Clicked 'Grant access'", username)
except Exception as e:
logger.warning(f"[{username}] No Grant access button: {e}")
logger.warning("[%s] No Grant access button: %s", username, e)
await page.screenshot(path=f"/tmp/login_flow_no_grant_{username}.png")
# Step 4: Password confirmation dialog
@@ -841,27 +845,27 @@ async def _complete_login_flow_v2_as_user(
)
try:
await confirm_password.wait_for(timeout=10000)
logger.info(f"[{username}] Password confirmation dialog detected")
logger.info("[%s] Password confirmation dialog detected", username)
await confirm_password.fill(password)
confirm_btn = page.get_by_role("dialog").get_by_role(
"button", name="Confirm"
)
await confirm_btn.wait_for(timeout=5000)
await confirm_btn.click()
logger.info(f"[{username}] Clicked 'Confirm' in password dialog")
logger.info("[%s] Clicked 'Confirm' in password dialog", username)
except Exception:
logger.info(
f"[{username}] No password confirmation dialog "
"(may have been auto-confirmed)"
"[%s] No password confirmation dialog (may have been auto-confirmed)",
username,
)
# Step 5: Wait for "Account connected" success page
try:
await page.get_by_text("Account connected").wait_for(timeout=15000)
logger.info(f"[{username}] Login Flow v2 completed: Account connected!")
logger.info("[%s] Login Flow v2 completed: Account connected!", username)
except Exception:
await page.wait_for_load_state("networkidle", timeout=10000)
logger.info(f"[{username}] Login Flow v2 done. Final URL: {page.url}")
logger.info("[%s] Login Flow v2 done. Final URL: %s", username, page.url)
finally:
await context.close()
@@ -986,7 +990,9 @@ async def login_flow_static_client_credentials(anyio_backend, oauth_callback_ser
capture_output=True,
)
logger.info(f"Creating static OIDC client {client_id} with callback {callback_url}")
logger.info(
"Creating static OIDC client %s with callback %s", client_id, callback_url
)
result = subprocess.run(
[
"docker",
@@ -61,9 +61,9 @@ async def test_dcr_deletion_authentication_methods(
)
deletion_endpoint = f"{nextcloud_host}/apps/oidc/register/{client_info.client_id}"
logger.info(f"\nTesting deletion endpoint: {deletion_endpoint}")
logger.info(f"Client ID: {client_info.client_id}")
logger.info(f"Client Secret (first 16 chars): {client_info.client_secret[:16]}...")
logger.info("\\nTesting deletion endpoint: %s", deletion_endpoint)
logger.info("Client ID: %s", client_info.client_id)
logger.info("Client Secret (first 16 chars): %s...", client_info.client_secret[:16])
results = {}
@@ -79,11 +79,11 @@ async def test_dcr_deletion_authentication_methods(
"status": response.status_code,
"body": response.text[:200],
}
logger.info(f"Status: {response.status_code}")
logger.info(f"Body: {response.text[:200]}")
logger.info("Status: %s", response.status_code)
logger.info("Body: %s", response.text[:200])
except Exception as e:
results["basic_auth"] = {"status": "error", "error": str(e)}
logger.error(f"Error: {e}")
logger.error("Error: %s", e)
# Method 2: Credentials in JSON body
logger.info("\n=== Method 2: Credentials in JSON Body ===")
@@ -99,11 +99,11 @@ async def test_dcr_deletion_authentication_methods(
"status": response.status_code,
"body": response.text[:200],
}
logger.info(f"Status: {response.status_code}")
logger.info(f"Body: {response.text[:200]}")
logger.info("Status: %s", response.status_code)
logger.info("Body: %s", response.text[:200])
except Exception as e:
results["json_body"] = {"status": "error", "error": str(e)}
logger.error(f"Error: {e}")
logger.error("Error: %s", e)
# Method 3: Credentials in query parameters
logger.info("\n=== Method 3: Credentials in Query Parameters ===")
@@ -119,11 +119,11 @@ async def test_dcr_deletion_authentication_methods(
"status": response.status_code,
"body": response.text[:200],
}
logger.info(f"Status: {response.status_code}")
logger.info(f"Body: {response.text[:200]}")
logger.info("Status: %s", response.status_code)
logger.info("Body: %s", response.text[:200])
except Exception as e:
results["query_params"] = {"status": "error", "error": str(e)}
logger.error(f"Error: {e}")
logger.error("Error: %s", e)
# Method 4: No authentication (baseline)
logger.info("\n=== Method 4: No Authentication (Baseline) ===")
@@ -133,11 +133,11 @@ async def test_dcr_deletion_authentication_methods(
"status": response.status_code,
"body": response.text[:200],
}
logger.info(f"Status: {response.status_code}")
logger.info(f"Body: {response.text[:200]}")
logger.info("Status: %s", response.status_code)
logger.info("Body: %s", response.text[:200])
except Exception as e:
results["no_auth"] = {"status": "error", "error": str(e)}
logger.error(f"Error: {e}")
logger.error("Error: %s", e)
# Print summary
logger.info("\n" + "=" * 70)
@@ -146,7 +146,7 @@ async def test_dcr_deletion_authentication_methods(
for method, result in results.items():
status = result.get("status", "unknown")
logger.info(f"{method:20s} → Status: {status}")
logger.info("%s → Status: %s", format(method, "20s"), status)
# Analysis
logger.info("\n" + "=" * 70)
@@ -170,11 +170,11 @@ async def test_dcr_deletion_authentication_methods(
logger.info("✓ At least one authentication method succeeded (204 No Content)")
for method, result in results.items():
if result.get("status") == 204:
logger.info(f" Working method: {method}")
logger.info(" Working method: %s", method)
else:
logger.info("? Mixed results - further investigation needed")
for method, result in results.items():
logger.info(f" {method}: {result.get('status')}")
logger.info(" %s: %s", method, result.get("status"))
# Document the finding
assert all_401 or any_204, (
+18 -16
View File
@@ -101,7 +101,7 @@ async def get_oauth_token_with_client(
try:
await _handle_oauth_consent_screen(page, username)
except Exception as e:
logger.debug(f"No consent screen or already authorized: {e}")
logger.debug("No consent screen or already authorized: %s", e)
# Wait for callback
logger.info("Waiting for OAuth callback...")
@@ -115,7 +115,7 @@ async def get_oauth_token_with_client(
await anyio.sleep(0.5)
auth_code = auth_states[state]
logger.info(f"Got auth code: {auth_code[:20]}...")
logger.info("Got auth code: %s...", auth_code[:20])
finally:
await context.close()
@@ -200,8 +200,8 @@ async def test_dcr_register_and_delete_lifecycle(
reg_response.raise_for_status()
full_client_info = reg_response.json()
logger.info(f"Full registration response keys: {list(full_client_info.keys())}")
logger.info(f"Registration response: {full_client_info}")
logger.info("Full registration response keys: %s", list(full_client_info.keys()))
logger.info("Registration response: %s", full_client_info)
# Use the register_client function for the ClientInfo object
client_info = await register_client(
@@ -217,13 +217,13 @@ async def test_dcr_register_and_delete_lifecycle(
registration_access_token = full_client_info.get("registration_access_token")
registration_client_uri = full_client_info.get("registration_client_uri")
logger.info(
f"Registration access token present: {registration_access_token is not None}"
"Registration access token present: %s", registration_access_token is not None
)
logger.info(
f"Registration client URI present: {registration_client_uri is not None}"
"Registration client URI present: %s", registration_client_uri is not None
)
logger.info(f"✅ Client registered: {client_info.client_id[:16]}...")
logger.info("✅ Client registered: %s...", client_info.client_id[:16])
# Step 2: Obtain token and verify client works
logger.info("Step 2: Obtaining OAuth token with registered client...")
@@ -239,14 +239,15 @@ async def test_dcr_register_and_delete_lifecycle(
)
assert access_token, "Failed to obtain access token"
logger.info(f"✅ Access token obtained: {access_token[:30]}...")
logger.info("✅ Access token obtained: %s...", access_token[:30])
# Step 3: Delete the client using RFC 7592
logger.info("Step 3: Deleting OAuth client...")
logger.info(f"Client ID: {client_info.client_id}")
logger.info(f"Client secret (first 16 chars): {client_info.client_secret[:16]}...")
logger.info("Client ID: %s", client_info.client_id)
logger.info("Client secret (first 16 chars): %s...", client_info.client_secret[:16])
logger.info(
f"Registration access token: {registration_access_token[:16] if registration_access_token else 'None'}..."
"Registration access token: %s...",
registration_access_token[:16] if registration_access_token else "None",
)
# Use delete_client() which prefers RFC 7592 Bearer token, falls back to Basic Auth
@@ -261,7 +262,7 @@ async def test_dcr_register_and_delete_lifecycle(
assert success, (
"Client deletion should succeed with RFC 7592 Bearer token or Basic Auth"
)
logger.info(f"✅ Client deleted successfully: {client_info.client_id[:16]}...")
logger.info("✅ Client deleted successfully: %s...", client_info.client_id[:16])
# Step 4: Verify deleted client cannot obtain new tokens
logger.info("Step 4: Verifying deleted client cannot obtain new tokens...")
@@ -284,7 +285,8 @@ async def test_dcr_register_and_delete_lifecycle(
# Accept either 400 (Bad Request) or 401 (Unauthorized) as valid rejection
if token_response.status_code in [400, 401]:
logger.info(
f"✅ Deleted client correctly rejected ({token_response.status_code})"
"✅ Deleted client correctly rejected (%s)",
token_response.status_code,
)
else:
# Unexpected success - client should be deleted
@@ -343,7 +345,7 @@ async def test_dcr_delete_with_wrong_credentials(
token_type="Bearer",
)
logger.info(f"Client registered: {client_info.client_id[:16]}...")
logger.info("Client registered: %s...", client_info.client_id[:16])
# Try to delete with wrong registration_access_token (RFC 7592 Bearer token)
logger.info("Attempting deletion with wrong registration_access_token...")
@@ -392,7 +394,7 @@ async def test_dcr_delete_nonexistent_client(
fake_client_id = "nonexistent_" + secrets.token_urlsafe(16)
fake_client_secret = secrets.token_urlsafe(32)
logger.info(f"Attempting to delete non-existent client: {fake_client_id[:16]}...")
logger.info("Attempting to delete non-existent client: %s...", fake_client_id[:16])
success = await delete_client(
nextcloud_url=nextcloud_host,
@@ -442,7 +444,7 @@ async def test_dcr_deletion_is_idempotent(
token_type="Bearer",
)
logger.info(f"Client registered: {client_info.client_id[:16]}...")
logger.info("Client registered: %s...", client_info.client_id[:16])
# First deletion with RFC 7592 Bearer token
logger.info("First deletion attempt...")
@@ -63,28 +63,28 @@ async def test_new_dcr_registration_includes_access_token(
registration_data = response.json()
# Log the full response
logger.info(f"\n{'=' * 70}")
logger.info("\\n%s", "=" * 70)
logger.info("REGISTRATION RESPONSE")
logger.info(f"{'=' * 70}")
logger.info(f"Response keys: {sorted(registration_data.keys())}")
logger.info("%s", "=" * 70)
logger.info("Response keys: %s", sorted(registration_data.keys()))
logger.info("\nFull response:")
for key, value in sorted(registration_data.items()):
if key in ["client_secret", "registration_access_token"]:
# Truncate secrets for security
logger.info(f" {key}: {value[:20]}... (truncated)")
logger.info(" %s: %s... (truncated)", key, value[:20])
else:
logger.info(f" {key}: {value}")
logger.info(" %s: %s", key, value)
# Check for RFC 7592 required fields
logger.info(f"\n{'=' * 70}")
logger.info("\\n%s", "=" * 70)
logger.info("RFC 7592 COMPLIANCE CHECK")
logger.info(f"{'=' * 70}")
logger.info("%s", "=" * 70)
has_token = "registration_access_token" in registration_data
has_uri = "registration_client_uri" in registration_data
logger.info(f"registration_access_token present: {has_token}")
logger.info(f"registration_client_uri present: {has_uri}")
logger.info("registration_access_token present: %s", has_token)
logger.info("registration_client_uri present: %s", has_uri)
if has_token and has_uri:
logger.info(
@@ -100,15 +100,15 @@ async def test_new_dcr_registration_includes_access_token(
registration_client_uri = registration_data.get("registration_client_uri")
# Now test deletion with the registration_access_token
logger.info(f"\n{'=' * 70}")
logger.info("\\n%s", "=" * 70)
logger.info("TESTING DCR DELETION WITH REGISTRATION_ACCESS_TOKEN")
logger.info(f"{'=' * 70}")
logger.info("%s", "=" * 70)
deletion_endpoint = (
registration_client_uri
or f"{nextcloud_host}/apps/oidc/register/{client_id}"
)
logger.info(f"Deletion endpoint: {deletion_endpoint}")
logger.info("Deletion endpoint: %s", deletion_endpoint)
async with httpx.AsyncClient(timeout=30.0) as client:
# Try deletion with Bearer token (RFC 7592 standard)
@@ -118,8 +118,8 @@ async def test_new_dcr_registration_includes_access_token(
headers={"Authorization": f"Bearer {registration_access_token}"},
)
logger.info(f"Response status: {delete_response.status_code}")
logger.info(f"Response body: {delete_response.text[:200]}")
logger.info("Response status: %s", delete_response.status_code)
logger.info("Response body: %s", delete_response.text[:200])
if delete_response.status_code == 204:
logger.info(
@@ -139,7 +139,7 @@ async def test_new_dcr_registration_includes_access_token(
)
else:
logger.warning(
f"\n? UNEXPECTED: Got status {delete_response.status_code}"
"\\n? UNEXPECTED: Got status %s", delete_response.status_code
)
pytest.fail(
f"Unexpected status code: {delete_response.status_code}, body: {delete_response.text[:500]}"
@@ -204,10 +204,10 @@ async def test_dcr_deletion_with_basic_auth_new_impl(
client_secret = reg_data["client_secret"]
deletion_endpoint = f"{nextcloud_host}/apps/oidc/register/{client_id}"
logger.info(f"\n{'=' * 70}")
logger.info("\\n%s", "=" * 70)
logger.info("TESTING DCR DELETION WITH HTTP BASIC AUTH")
logger.info(f"{'=' * 70}")
logger.info(f"Endpoint: {deletion_endpoint}")
logger.info("%s", "=" * 70)
logger.info("Endpoint: %s", deletion_endpoint)
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.delete(
@@ -215,8 +215,8 @@ async def test_dcr_deletion_with_basic_auth_new_impl(
auth=(client_id, client_secret),
)
logger.info(f"Status: {response.status_code}")
logger.info(f"Body: {response.text[:200]}")
logger.info("Status: %s", response.status_code)
logger.info("Body: %s", response.text[:200])
if response.status_code == 204:
logger.info("\n✓ SUCCESS: HTTP Basic Auth works for deletion!")
@@ -225,7 +225,7 @@ async def test_dcr_deletion_with_basic_auth_new_impl(
"\n✗ HTTP Basic Auth not supported - use registration_access_token instead"
)
else:
logger.warning(f"\n? Unexpected status: {response.status_code}")
logger.warning("\\n? Unexpected status: %s", response.status_code)
# This test is informational - we don't fail if Basic Auth doesn't work
# as long as Bearer token works
+10 -10
View File
@@ -157,7 +157,7 @@ async def get_oauth_token_with_client(
try:
await _handle_oauth_consent_screen(page, username)
except Exception as e:
logger.debug(f"No consent screen or already authorized: {e}")
logger.debug("No consent screen or already authorized: %s", e)
# Wait for callback
logger.info("Waiting for OAuth callback...")
@@ -171,7 +171,7 @@ async def get_oauth_token_with_client(
await anyio.sleep(0.5)
auth_code = auth_states[state]
logger.info(f"Got auth code: {auth_code[:20]}...")
logger.info("Got auth code: %s...", auth_code[:20])
finally:
await context.close()
@@ -245,7 +245,7 @@ async def test_dcr_respects_jwt_token_type(
token_type="jwt",
)
logger.info(f"Registered JWT client: {client_info.client_id[:16]}...")
logger.info("Registered JWT client: %s...", client_info.client_id[:16])
# Obtain token via OAuth flow
access_token = await get_oauth_token_with_client(
@@ -280,8 +280,8 @@ async def test_dcr_respects_jwt_token_type(
assert "notes.write" in scopes, "JWT scope claim missing notes.write"
logger.info(
f"✅ DCR with token_type=jwt works correctly! "
f"Token is JWT format with scope claim: {payload['scope']}"
"✅ DCR with token_type=jwt works correctly! Token is JWT format with scope claim: %s",
payload["scope"],
)
@@ -329,7 +329,7 @@ async def test_dcr_respects_bearer_token_type(
token_type="opaque",
)
logger.info(f"Registered Opaque token client: {client_info.client_id[:16]}...")
logger.info("Registered Opaque token client: %s...", client_info.client_id[:16])
# Obtain token via OAuth flow
access_token = await get_oauth_token_with_client(
@@ -357,8 +357,8 @@ async def test_dcr_respects_bearer_token_type(
pass
logger.info(
f"✅ DCR with token_type=opaque works correctly! "
f"Token is opaque (not JWT format): {access_token[:30]}..."
"✅ DCR with token_type=opaque works correctly! Token is opaque (not JWT format): %s...",
access_token[:30],
)
@@ -390,8 +390,8 @@ async def test_jwt_tokens_embed_scopes_in_payload():
# but we document the behavior explicitly here for reference
logger.info(
"✅ JWT token scope embedding verified. "
f"Expected scopes in JWT payload: {DEFAULT_FULL_SCOPES}"
"✅ JWT token scope embedding verified. Expected scopes in JWT payload: %s",
DEFAULT_FULL_SCOPES,
)
# This test primarily serves as documentation
@@ -82,7 +82,7 @@ async def test_oauth_clients(
token_type="Bearer", # Use opaque tokens for this test
)
clients["clientA"] = (client_a.client_id, client_a.client_secret)
logger.info(f"Created client A: {client_a.client_id[:16]}...")
logger.info("Created client A: %s...", client_a.client_id[:16])
# Create client B (will attempt to introspect client A's tokens)
logger.info("Creating OAuth client B for introspection testing")
@@ -95,7 +95,7 @@ async def test_oauth_clients(
token_type="Bearer",
)
clients["clientB"] = (client_b.client_id, client_b.client_secret)
logger.info(f"Created client B: {client_b.client_id[:16]}...")
logger.info("Created client B: %s...", client_b.client_id[:16])
# Create client C (third party, should not be able to introspect)
logger.info("Creating OAuth client C for introspection testing")
@@ -108,7 +108,7 @@ async def test_oauth_clients(
token_type="Bearer",
)
clients["clientC"] = (client_c.client_id, client_c.client_secret)
logger.info(f"Created client C: {client_c.client_id[:16]}...")
logger.info("Created client C: %s...", client_c.client_id[:16])
yield clients
@@ -146,7 +146,7 @@ async def test_introspection_requires_client_authentication(
)
assert response.status_code == 401, "Should return 401 with invalid credentials"
data = response.json()
logger.info(f"Invalid client response: {data}")
logger.info("Invalid client response: %s", data)
# Response may be either {"error": "invalid_client"} or {"message": "..."}
# Both are acceptable as long as we get 401
assert "error" in data or "message" in data, "Should return error information"
@@ -191,19 +191,21 @@ async def _obtain_token_for_client(
auth_url = "".join(auth_url_parts)
logger.info(f"Obtaining token for client {client_id[:16]}... with scopes={scope}")
logger.info(
"Obtaining token for client %s... with scopes=%s", client_id[:16], scope
)
if resource:
logger.info(f" Resource parameter: {resource[:16]}...")
logger.info(" Resource parameter: %s...", resource[:16])
# Browser automation (same pattern as conftest.py)
context = await browser.new_context(ignore_https_errors=True)
page = await context.new_page()
try:
logger.debug(f"Navigating to: {auth_url[:100]}...")
logger.debug("Navigating to: %s...", auth_url[:100])
await page.goto(auth_url, wait_until="networkidle", timeout=60000)
current_url = page.url
logger.debug(f"Current URL after navigation: {current_url}")
logger.debug("Current URL after navigation: %s", current_url)
# Handle login if needed
if "/login" in current_url or "/index.php/login" in current_url:
@@ -214,24 +216,24 @@ async def _obtain_token_for_client(
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle", timeout=60000)
current_url = page.url
logger.info(f"After login: {current_url}")
logger.info("After login: %s", current_url)
# Wait a bit for page to fully render after login
await anyio.sleep(2)
current_url = page.url
logger.info(f"After waiting, current URL: {current_url}")
logger.info("After waiting, current URL: %s", current_url)
# Check page content for debugging
page_content = await page.content()
has_consent_div = "#oidc-consent" in page_content
logger.info(f"Page has #oidc-consent div: {has_consent_div}")
logger.info("Page has #oidc-consent div: %s", has_consent_div)
# Handle consent screen using the helper from conftest
try:
consent_handled = await _handle_oauth_consent_screen(page, username)
logger.info(f"Consent screen handled: {consent_handled}")
logger.info("Consent screen handled: %s", consent_handled)
except Exception as e:
logger.warning(f"Error handling consent screen: {e}")
logger.warning("Error handling consent screen: %s", e)
# Take screenshot for debugging
await page.screenshot(path=f"/tmp/consent_error_{state[:8]}.png")
logger.error("Consent error screenshot saved")
@@ -247,15 +249,15 @@ async def _obtain_token_for_client(
f"/tmp/oauth_introspection_test_timeout_{state[:8]}.png"
)
await page.screenshot(path=screenshot_path)
logger.error(f"Timeout! Screenshot saved to {screenshot_path}")
logger.error(f"Current URL: {page.url}")
logger.error("Timeout! Screenshot saved to %s", screenshot_path)
logger.error("Current URL: %s", page.url)
raise TimeoutError(
f"Timeout waiting for OAuth callback (state={state[:16]}...)"
)
await anyio.sleep(0.5)
auth_code = auth_states[state]
logger.info(f"Successfully received auth code: {auth_code[:20]}...")
logger.info("Successfully received auth code: %s...", auth_code[:20])
finally:
await context.close()
@@ -311,10 +313,10 @@ async def test_client_cannot_introspect_other_clients_tokens(
different_client_id, different_client_secret = test_oauth_clients["clientB"]
logger.info(
f"Testing introspection with shared client token: {access_token[:16]}..."
"Testing introspection with shared client token: %s...", access_token[:16]
)
logger.info(f"Shared client ID: {shared_client_id[:16]}...")
logger.info(f"Different client ID: {different_client_id[:16]}...")
logger.info("Shared client ID: %s...", shared_client_id[:16])
logger.info("Different client ID: %s...", different_client_id[:16])
async with httpx.AsyncClient(timeout=10.0) as client:
# Test 1: The owning client (shared client) can introspect its own token
@@ -325,7 +327,7 @@ async def test_client_cannot_introspect_other_clients_tokens(
)
assert response.status_code == 200
data = response.json()
logger.info(f"Owner client introspection response: {data}")
logger.info("Owner client introspection response: %s", data)
assert data.get("active") is True, (
"Owner client should be able to introspect its own token"
)
@@ -338,7 +340,7 @@ async def test_client_cannot_introspect_other_clients_tokens(
)
assert response.status_code == 200
data = response.json()
logger.info(f"Different client introspection response: {data}")
logger.info("Different client introspection response: %s", data)
assert data.get("active") is False, (
"Different client should NOT be able to introspect another client's token"
)
@@ -387,11 +389,13 @@ async def test_introspection_with_resource_parameter(
resource=client_b_id, # Set client B as the resource server
)
except Exception as e:
logger.error(f"Failed to obtain token with resource parameter: {e}")
logger.error("Failed to obtain token with resource parameter: %s", e)
pytest.skip(f"Cannot obtain test token with resource parameter: {e}")
logger.info(
f"Obtained access token from client A with resource={client_b_id}: {access_token[:16]}..."
"Obtained access token from client A with resource=%s: %s...",
client_b_id,
access_token[:16],
)
# Test introspection
@@ -404,7 +408,7 @@ async def test_introspection_with_resource_parameter(
)
assert response.status_code == 200
data = response.json()
logger.info(f"Client A (owner) introspection response: {data}")
logger.info("Client A (owner) introspection response: %s", data)
assert data.get("active") is True, (
"Client A (owner) should be able to introspect its own token"
)
@@ -417,13 +421,13 @@ async def test_introspection_with_resource_parameter(
)
assert response.status_code == 200
data = response.json()
logger.info(f"Client B (resource server) introspection response: {data}")
logger.info("Client B (resource server) introspection response: %s", data)
assert data.get("active") is True, (
"Client B (resource server) should be able to introspect token intended for it"
)
# Verify the resource field in the response matches client B
logger.info(f"Full introspection response from Client B: {data}")
logger.info("Full introspection response from Client B: %s", data)
# Test 3: Client C CANNOT introspect the token (not owner, not resource server)
response = await client.post(
@@ -433,7 +437,7 @@ async def test_introspection_with_resource_parameter(
)
assert response.status_code == 200
data = response.json()
logger.info(f"Client C (third party) introspection response: {data}")
logger.info("Client C (third party) introspection response: %s", data)
assert data.get("active") is False, (
"Client C should NOT be able to introspect token (not owner or resource server)"
)
@@ -464,7 +468,7 @@ async def test_introspection_returns_inactive_for_invalid_token(
assert response.status_code == 200
data = response.json()
logger.info(f"Introspection response for fake token: {data}")
logger.info("Introspection response for fake token: %s", data)
assert data.get("active") is False, (
"Should return active=false for invalid token"
)
@@ -50,7 +50,7 @@ class TestLoginFlowAuthTools:
# Search" flow) stores. So accept either a non-empty list or None;
# the field's *presence* in the payload is what we care about here.
assert data["scopes"] is None or len(data["scopes"]) > 0
logger.info(f"Provisioned as: {data['username']}, scopes: {data['scopes']}")
logger.info("Provisioned as: %s, scopes: %s", data["username"], data["scopes"])
async def test_provision_access_already_provisioned(
self, nc_mcp_login_flow_client: ClientSession
@@ -100,7 +100,7 @@ class TestLoginFlowNotes:
note = json.loads(create_result.content[0].text)
note_id = note["id"]
etag = note["etag"]
logger.info(f"Created note {note_id}")
logger.info("Created note %s", note_id)
try:
# Read
@@ -152,7 +152,7 @@ class TestLoginFlowNotes:
await nc_mcp_login_flow_client.call_tool(
"nc_notes_delete_note", {"note_id": note_id}
)
logger.info(f"Deleted note {note_id}")
logger.info("Deleted note %s", note_id)
# ---------------------------------------------------------------------------
@@ -176,7 +176,7 @@ class TestLoginFlowCalendarEvents:
calendars = cal_data.get("calendars", [])
assert len(calendars) > 0
calendar_name = calendars[0].get("name", "personal")
logger.info(f"Using calendar: {calendar_name}")
logger.info("Using calendar: %s", calendar_name)
suffix = uuid.uuid4().hex[:8]
event_title = f"LoginFlow Event {suffix}"
@@ -197,7 +197,7 @@ class TestLoginFlowCalendarEvents:
)
event_data = json.loads(create_result.content[0].text)
event_uid = event_data.get("uid") or event_data.get("event_uid")
logger.info(f"Created event: {event_uid}")
logger.info("Created event: %s", event_uid)
try:
# Get event
@@ -213,7 +213,7 @@ class TestLoginFlowCalendarEvents:
"nc_calendar_delete_event",
{"calendar_name": calendar_name, "event_uid": event_uid},
)
logger.info(f"Deleted event {event_uid}")
logger.info("Deleted event %s", event_uid)
# ---------------------------------------------------------------------------
@@ -254,7 +254,7 @@ class TestLoginFlowCalendarTodos:
raise AssertionError(f"Create todo failed: {error_text}")
todo_data = json.loads(create_result.content[0].text)
todo_uid = todo_data.get("uid") or todo_data.get("todo_uid")
logger.info(f"Created todo: {todo_uid}")
logger.info("Created todo: %s", todo_uid)
try:
# List todos
@@ -280,7 +280,7 @@ class TestLoginFlowCalendarTodos:
"nc_calendar_delete_todo",
{"calendar_name": calendar_name, "todo_uid": todo_uid},
)
logger.info(f"Deleted todo {todo_uid}")
logger.info("Deleted todo %s", todo_uid)
# ---------------------------------------------------------------------------
@@ -312,7 +312,7 @@ class TestLoginFlowContacts:
assert create_ab_result.isError is False, (
f"Create addressbook failed: {create_ab_result.content[0].text}"
)
logger.info(f"Created address book: {ab_name}")
logger.info("Created address book: %s", ab_name)
try:
# Create contact (requires addressbook, uid, contact_data dict)
@@ -330,7 +330,7 @@ class TestLoginFlowContacts:
assert create_result.isError is False, (
f"Create contact failed: {create_result.content[0].text}"
)
logger.info(f"Created contact: {contact_uid}")
logger.info("Created contact: %s", contact_uid)
# List contacts in our clean addressbook
# Note: may fail due to server-side Pydantic bug where ContactField.value
@@ -343,7 +343,7 @@ class TestLoginFlowContacts:
error_text = list_result.content[0].text
if "ContactField" in error_text:
logger.warning(
f"Known server bug: ContactField validation: {error_text}"
"Known server bug: ContactField validation: %s", error_text
)
else:
raise AssertionError(f"List contacts failed: {error_text}")
@@ -360,7 +360,7 @@ class TestLoginFlowContacts:
"nc_contacts_delete_contact",
{"addressbook": ab_name, "uid": contact_uid},
)
logger.info(f"Deleted contact {contact_uid}")
logger.info("Deleted contact %s", contact_uid)
finally:
# Always clean up the temporary address book
@@ -368,7 +368,7 @@ class TestLoginFlowContacts:
"nc_contacts_delete_addressbook",
{"name": ab_name},
)
logger.info(f"Deleted address book {ab_name}")
logger.info("Deleted address book %s", ab_name)
# ---------------------------------------------------------------------------
@@ -393,7 +393,7 @@ class TestLoginFlowFiles:
assert mkdir_result.isError is False, (
f"Create dir failed: {mkdir_result.content[0].text}"
)
logger.info(f"Created directory: {dir_path}")
logger.info("Created directory: %s", dir_path)
try:
# Write file
@@ -436,7 +436,7 @@ class TestLoginFlowFiles:
await nc_mcp_login_flow_client.call_tool(
"nc_webdav_delete_resource", {"path": dir_path}
)
logger.info(f"Cleaned up {dir_path}")
logger.info("Cleaned up %s", dir_path)
# ---------------------------------------------------------------------------
@@ -467,7 +467,7 @@ class TestLoginFlowDeck:
)
board_data = json.loads(create_result.content[0].text)
board_id = board_data.get("id") or board_data.get("board_id")
logger.info(f"Created board: {board_id}")
logger.info("Created board: %s", board_id)
# List boards (tool name is deck_get_boards)
list_result = await nc_mcp_login_flow_client.call_tool(
@@ -499,9 +499,11 @@ class TestLoginFlowDeck:
resp = await client.delete(
f"/apps/deck/api/v1.0/boards/{board_id}"
)
logger.info(f"Board cleanup: {board_id}{resp.status_code}")
logger.info(
"Board cleanup: %s%s", board_id, resp.status_code
)
except Exception as e:
logger.warning(f"Board cleanup failed: {e}")
logger.warning("Board cleanup failed: %s", e)
# ---------------------------------------------------------------------------
@@ -521,7 +523,7 @@ class TestLoginFlowTables:
result = await nc_mcp_login_flow_client.call_tool("nc_tables_list_tables", {})
assert result.isError is False, f"List tables failed: {result.content[0].text}"
data = json.loads(result.content[0].text)
logger.info(f"Tables: {data}")
logger.info("Tables: %s", data)
# ---------------------------------------------------------------------------
@@ -569,7 +571,7 @@ class TestLoginFlowCookbook:
)
recipe_data = json.loads(create_result.content[0].text)
recipe_id = recipe_data.get("id") or recipe_data.get("recipe_id")
logger.info(f"Created recipe: {recipe_id}")
logger.info("Created recipe: %s", recipe_id)
try:
# Get recipe (may fail due to server-side Pydantic bug with recipeYield=None)
@@ -580,7 +582,8 @@ class TestLoginFlowCookbook:
error_text = get_result.content[0].text
if "recipeYield" in error_text:
logger.warning(
f"Known server bug: Recipe.recipeYield validation: {error_text}"
"Known server bug: Recipe.recipeYield validation: %s",
error_text,
)
else:
raise AssertionError(f"Get recipe failed: {error_text}")
@@ -590,7 +593,7 @@ class TestLoginFlowCookbook:
await nc_mcp_login_flow_client.call_tool(
"nc_cookbook_delete_recipe", {"recipe_id": recipe_id}
)
logger.info(f"Deleted recipe {recipe_id}")
logger.info("Deleted recipe %s", recipe_id)
# ---------------------------------------------------------------------------
@@ -655,4 +658,4 @@ class TestLoginFlowConnectivity:
async def test_list_resources(self, nc_mcp_login_flow_client: ClientSession):
"""Verify resource templates are available."""
templates = await nc_mcp_login_flow_client.list_resource_templates()
logger.info(f"Resource templates: {len(templates.resourceTemplates)}")
logger.info("Resource templates: %s", len(templates.resourceTemplates))
@@ -537,4 +537,4 @@ class TestMultiUserSmoke:
]:
tools = await client.list_tools()
assert len(tools.tools) > 0, f"{name} MCP client has no tools"
logger.info(f"{name} MCP client working ({len(tools.tools)} tools)")
logger.info("%s MCP client working (%s tools)", name, len(tools.tools))
@@ -73,7 +73,7 @@ async def test_read_only_token_filters_write_tools(nc_mcp_login_flow_client_read
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"Read-only token sees {len(tool_names)} tools")
logger.info("Read-only token sees %s tools", len(tool_names))
# Verify read tools are present (only for apps with :read scopes)
# Read-only token has: notes.read, calendar.read, contacts.read,
@@ -104,8 +104,8 @@ async def test_read_only_token_filters_write_tools(nc_mcp_login_flow_client_read
)
logger.info(
f"✅ Read-only token properly filters tools: {len(tool_names)} read tools visible, "
f"write tools hidden"
"✅ Read-only token properly filters tools: %s read tools visible, write tools hidden",
len(tool_names),
)
@@ -122,7 +122,7 @@ async def test_write_only_token_filters_read_tools(nc_mcp_login_flow_client_writ
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"Write-only token sees {len(tool_names)} tools")
logger.info("Write-only token sees %s tools", len(tool_names))
# Verify write tools are present
# Write-only token has: notes.write, calendar.write, contacts.write,
@@ -153,8 +153,8 @@ async def test_write_only_token_filters_read_tools(nc_mcp_login_flow_client_writ
)
logger.info(
f"✅ Write-only token properly filters tools: {len(tool_names)} write tools visible, "
f"read tools hidden"
"✅ Write-only token properly filters tools: %s write tools visible, read tools hidden",
len(tool_names),
)
@@ -171,8 +171,8 @@ async def test_full_access_token_shows_all_tools(nc_mcp_login_flow_client_full_a
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"Full access token sees {len(tool_names)} tools")
logger.info(f"Tools: {sorted(tool_names)}")
logger.info("Full access token sees %s tools", len(tool_names))
logger.info("Tools: %s", sorted(tool_names))
# Verify both read and write tools are present
# Full access has all *read and *write scopes
@@ -197,7 +197,7 @@ async def test_full_access_token_shows_all_tools(nc_mcp_login_flow_client_full_a
assert len(tool_names) >= 90
logger.info(
f"✅ Full access token sees all tools: {len(tool_names)} total (read + write)"
"✅ Full access token sees all tools: %s total (read + write)", len(tool_names)
)
@@ -415,7 +415,8 @@ async def test_jwt_with_no_custom_scopes_returns_zero_tools(
tool_names = [tool.name for tool in result.tools]
logger.info(
f"JWT token with no custom scopes sees {len(tool_names)} tools (should be 7 auth tools)"
"JWT token with no custom scopes sees %s tools (should be 7 auth tools)",
len(tool_names),
)
# Only auth/provisioning tools should be visible (they require 'openid' scope)
@@ -435,8 +436,8 @@ async def test_jwt_with_no_custom_scopes_returns_zero_tools(
)
logger.info(
f"✅ JWT token with only openid scope correctly shows {len(tool_names)} auth tools, "
"resource tools filtered out"
"✅ JWT token with only openid scope correctly shows %s auth tools, resource tools filtered out",
len(tool_names),
)
@@ -457,7 +458,7 @@ async def test_jwt_consent_scenarios_read_only(nc_mcp_login_flow_client_read_onl
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"JWT with nc:read consent sees {len(tool_names)} tools")
logger.info("JWT with nc:read consent sees %s tools", len(tool_names))
# Verify read tools are present
read_tools = ["nc_notes_get_note", "nc_notes_search_notes", "nc_webdav_read_file"]
@@ -474,7 +475,8 @@ async def test_jwt_consent_scenarios_read_only(nc_mcp_login_flow_client_read_onl
assert tool not in tool_names, f"Write tool {tool} should be filtered out"
logger.info(
f"✅ JWT with nc:read consent: {len(tool_names)} read tools visible, write tools filtered"
"✅ JWT with nc:read consent: %s read tools visible, write tools filtered",
len(tool_names),
)
@@ -495,7 +497,7 @@ async def test_jwt_consent_scenarios_write_only(nc_mcp_login_flow_client_write_o
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"JWT with nc:write consent sees {len(tool_names)} tools")
logger.info("JWT with nc:write consent sees %s tools", len(tool_names))
# Verify write tools are present
write_tools = [
@@ -512,7 +514,8 @@ async def test_jwt_consent_scenarios_write_only(nc_mcp_login_flow_client_write_o
assert tool not in tool_names, f"Read-only tool {tool} should be filtered out"
logger.info(
f"✅ JWT with nc:write consent: {len(tool_names)} write tools visible, read-only tools filtered"
"✅ JWT with nc:write consent: %s write tools visible, read-only tools filtered",
len(tool_names),
)
@@ -533,7 +536,7 @@ async def test_jwt_consent_scenarios_full_access(nc_mcp_login_flow_client_full_a
assert len(result.tools) > 0
tool_names = [tool.name for tool in result.tools]
logger.info(f"JWT with full consent sees {len(tool_names)} tools")
logger.info("JWT with full consent sees %s tools", len(tool_names))
# Verify both read and write tools are present
read_tools = ["nc_notes_get_note", "nc_webdav_read_file"]
@@ -549,7 +552,7 @@ async def test_jwt_consent_scenarios_full_access(nc_mcp_login_flow_client_full_a
assert len(tool_names) >= 90, f"Expected 90+ tools but got {len(tool_names)}"
logger.info(
f"✅ JWT with full consent: {len(tool_names)} tools visible (all read + write)"
"✅ JWT with full consent: %s tools visible (all read + write)", len(tool_names)
)
+1 -1
View File
@@ -40,7 +40,7 @@ async def test_mcp_update_event_extended_fields(
result_data = json.loads(create_result.content[0].text)
event_uid = result_data["uid"]
logger.info(f"Created base event via MCP: {event_uid}")
logger.info("Created base event via MCP: %s", event_uid)
# 2. Update with all four extended fields via MCP
update_result = await nc_mcp_client.call_tool(
+5 -5
View File
@@ -24,7 +24,7 @@ async def test_mcp_todo_complete_workflow(
try:
# 1. Create todo via MCP
logger.info(f"Creating todo in {calendar_name} via MCP")
logger.info("Creating todo in %s via MCP", calendar_name)
tomorrow = datetime.now() + timedelta(days=1)
create_result = await nc_mcp_client.call_tool(
@@ -46,7 +46,7 @@ async def test_mcp_todo_complete_workflow(
result_json = json.loads(result_data)
todo_uid = result_json["uid"]
logger.info(f"Created todo with UID: {todo_uid}")
logger.info("Created todo with UID: %s", todo_uid)
# 2. Verify todo creation via client
todos = await nc_client.calendar.list_todos(calendar_name)
@@ -57,7 +57,7 @@ async def test_mcp_todo_complete_workflow(
assert created_todo["priority"] == 3
# 3. List todos via MCP
logger.info(f"Listing todos in {calendar_name} via MCP")
logger.info("Listing todos in %s via MCP", calendar_name)
list_result = await nc_mcp_client.call_tool(
"nc_calendar_list_todos",
{"calendar_name": calendar_name},
@@ -69,7 +69,7 @@ async def test_mcp_todo_complete_workflow(
assert any(t["uid"] == todo_uid for t in list_data["todos"])
# 4. Update todo via MCP
logger.info(f"Updating todo {todo_uid} via MCP")
logger.info("Updating todo %s via MCP", todo_uid)
update_result = await nc_mcp_client.call_tool(
"nc_calendar_update_todo",
{
@@ -92,7 +92,7 @@ async def test_mcp_todo_complete_workflow(
assert updated_todo["percent_complete"] == 50
# 6. Delete todo via MCP
logger.info(f"Deleting todo {todo_uid} via MCP")
logger.info("Deleting todo %s via MCP", todo_uid)
delete_result = await nc_mcp_client.call_tool(
"nc_calendar_delete_todo",
{"calendar_name": calendar_name, "todo_uid": todo_uid},
+16 -16
View File
@@ -27,7 +27,7 @@ async def temporary_collective(nc_mcp_client: ClientSession):
assert result.isError is False, f"Failed to create collective: {result.content}"
data = json.loads(result.content[0].text)
collective_id = data["id"]
logger.info(f"Created temporary collective: {name} (ID: {collective_id})")
logger.info("Created temporary collective: %s (ID: %s)", name, collective_id)
# Get the landing page ID — filter by parentId == 0 (root page)
pages_result = await nc_mcp_client.call_tool(
@@ -55,9 +55,9 @@ async def temporary_collective(nc_mcp_client: ClientSession):
"collectives_delete_collective",
{"collective_id": collective_id},
)
logger.info(f"Cleaned up collective: {collective_id}")
logger.info("Cleaned up collective: %s", collective_id)
except Exception as e:
logger.warning(f"Cleanup of collective {collective_id} failed: {e}")
logger.warning("Cleanup of collective %s failed: %s", collective_id, e)
# --- Tool Discovery ---
@@ -96,7 +96,7 @@ async def test_collectives_tools_available(nc_mcp_client: ClientSession):
f"Expected tool '{expected}' not found in available tools"
)
logger.info(f"All {len(expected_tools)} Collectives tools registered")
logger.info("All %s Collectives tools registered", len(expected_tools))
# --- Collective CRUD ---
@@ -115,7 +115,7 @@ async def test_collectives_list(
collective_ids = [c["id"] for c in data["collectives"]]
assert temporary_collective["id"] in collective_ids
logger.info(f"Found {data['total']} collectives")
logger.info("Found %s collectives", data["total"])
async def test_collectives_set_collective_emoji(
@@ -179,7 +179,7 @@ async def test_collectives_page_workflow(
page_id = create_data["id"]
assert create_data["collective_id"] == cid
assert create_data["parent_id"] == landing_id
logger.info(f"Created page: {unique_title} (ID: {page_id})")
logger.info("Created page: %s (ID: %s)", unique_title, page_id)
# 2. List pages — should include the new page
list_result = await nc_mcp_client.call_tool(
@@ -190,7 +190,7 @@ async def test_collectives_page_workflow(
list_data = json.loads(list_result.content[0].text)
page_ids = [p["id"] for p in list_data["pages"]]
assert page_id in page_ids
logger.info(f"Page found in list ({list_data['total']} pages)")
logger.info("Page found in list (%s pages)", list_data["total"])
# 3. Get page with content
get_result = await nc_mcp_client.call_tool(
@@ -269,7 +269,7 @@ async def test_collectives_get_landing_page_content(
"Landing page should have auto-generated content"
)
assert len(data["content"]) > 0, "Landing page should have non-empty content"
logger.info(f"Landing page content: {len(data['content'])} bytes")
logger.info("Landing page content: %s bytes", len(data["content"]))
async def test_collectives_move_page(
@@ -307,7 +307,7 @@ async def test_collectives_move_page(
assert data["page_id"] == page_id
assert "moved" in data["message"]
assert new_title in data["message"]
logger.info(f"Page renamed to: {new_title}")
logger.info("Page renamed to: %s", new_title)
# Cleanup
await nc_mcp_client.call_tool(
@@ -337,7 +337,7 @@ async def test_collectives_tag_workflow(
tag_id = tag_data["id"]
assert tag_data["name"] == tag_name
assert tag_data["color"] == "FF5733"
logger.info(f"Created tag: {tag_name} (ID: {tag_id})")
logger.info("Created tag: %s (ID: %s)", tag_name, tag_id)
# 2. List tags — should include the new tag
list_tags_result = await nc_mcp_client.call_tool(
@@ -348,7 +348,7 @@ async def test_collectives_tag_workflow(
tags_data = json.loads(list_tags_result.content[0].text)
tag_ids = [t["id"] for t in tags_data["tags"]]
assert tag_id in tag_ids
logger.info(f"Tag found in list ({tags_data['total']} tags)")
logger.info("Tag found in list (%s tags)", tags_data["total"])
# 3. Create a page to tag
page_result = await nc_mcp_client.call_tool(
@@ -368,7 +368,7 @@ async def test_collectives_tag_workflow(
{"collective_id": cid, "page_id": page_id, "tag_id": tag_id},
)
assert assign_result.isError is False
logger.info(f"Tag {tag_id} assigned to page {page_id}")
logger.info("Tag %s assigned to page %s", tag_id, page_id)
# 5. Remove tag from page
remove_result = await nc_mcp_client.call_tool(
@@ -376,7 +376,7 @@ async def test_collectives_tag_workflow(
{"collective_id": cid, "page_id": page_id, "tag_id": tag_id},
)
assert remove_result.isError is False
logger.info(f"Tag {tag_id} removed from page {page_id}")
logger.info("Tag %s removed from page %s", tag_id, page_id)
# Cleanup
await nc_mcp_client.call_tool(
@@ -406,7 +406,7 @@ async def test_collectives_search(
assert data["query"] == "Welcome"
assert data["collective_id"] == cid
# Search may or may not find results depending on indexing timing
logger.info(f"Search returned {data['total']} results for 'Welcome'")
logger.info("Search returned %s results for 'Welcome'", data["total"])
# --- Collective Trash / Restore / Delete ---
@@ -425,7 +425,7 @@ async def test_collectives_trash_restore_delete_workflow(
assert create_result.isError is False
created = json.loads(create_result.content[0].text)
cid = created["id"]
logger.info(f"Created collective {name} (ID: {cid})")
logger.info("Created collective %s (ID: %s)", name, cid)
# Trash the collective
trash_result = await nc_mcp_client.call_tool(
@@ -444,7 +444,7 @@ async def test_collectives_trash_restore_delete_workflow(
trash_data = json.loads(list_trash_result.content[0].text)
trashed_ids = [c["id"] for c in trash_data["collectives"]]
assert cid in trashed_ids
logger.info(f"Found {trash_data['total']} trashed collectives")
logger.info("Found %s trashed collectives", trash_data["total"])
# Restore the collective
restore_result = await nc_mcp_client.call_tool(
+4 -4
View File
@@ -28,7 +28,7 @@ async def test_mcp_contacts_workflow(
try:
# 1. Create address book via MCP
logger.info(f"Creating address book via MCP: {addressbook_name}")
logger.info("Creating address book via MCP: %s", addressbook_name)
create_ab_result = await nc_mcp_client.call_tool(
"nc_contacts_create_addressbook",
{"name": addressbook_name, "display_name": f"MCP Test {addressbook_name}"},
@@ -40,7 +40,7 @@ async def test_mcp_contacts_workflow(
assert any(ab["name"] == addressbook_name for ab in addressbooks)
# 3. Create contact via MCP
logger.info(f"Creating contact in {addressbook_name} via MCP")
logger.info("Creating contact in %s via MCP", addressbook_name)
create_c_result = await nc_mcp_client.call_tool(
"nc_contacts_create_contact",
{
@@ -56,7 +56,7 @@ async def test_mcp_contacts_workflow(
assert any(c["vcard_id"] == contact_uid for c in contacts)
# 5. Delete contact via MCP
logger.info(f"Deleting contact {contact_uid} via MCP")
logger.info("Deleting contact %s via MCP", contact_uid)
delete_c_result = await nc_mcp_client.call_tool(
"nc_contacts_delete_contact",
{"addressbook": addressbook_name, "uid": contact_uid},
@@ -68,7 +68,7 @@ async def test_mcp_contacts_workflow(
assert not any(c["vcard_id"] == contact_uid for c in contacts)
# 7. Delete address book via MCP
logger.info(f"Deleting address book {addressbook_name} via MCP")
logger.info("Deleting address book %s via MCP", addressbook_name)
delete_ab_result = await nc_mcp_client.call_tool(
"nc_contacts_delete_addressbook", {"name": addressbook_name}
)
+48 -44
View File
@@ -36,7 +36,7 @@ async def test_mcp_cookbook_create_and_read_recipe(
try:
# 1. Create recipe via MCP
logger.info(f"Creating recipe via MCP: {recipe_name}")
logger.info("Creating recipe via MCP: %s", recipe_name)
create_result = await nc_mcp_client.call_tool(
"nc_cookbook_create_recipe",
{
@@ -59,7 +59,7 @@ async def test_mcp_cookbook_create_and_read_recipe(
create_response = json.loads(create_result.content[0].text)
created_recipe_id = create_response["id"]
logger.info(f"Recipe created via MCP with ID: {created_recipe_id}")
logger.info("Recipe created via MCP with ID: %s", created_recipe_id)
# 2. Verify creation via direct NextcloudClient
direct_recipe = await nc_client.cookbook.get_recipe(created_recipe_id)
@@ -70,7 +70,7 @@ async def test_mcp_cookbook_create_and_read_recipe(
assert direct_recipe["recipeCategory"] == "MCPTesting"
# 3. Read recipe via MCP
logger.info(f"Reading recipe via MCP: {created_recipe_id}")
logger.info("Reading recipe via MCP: %s", created_recipe_id)
read_result = await nc_mcp_client.call_tool(
"nc_cookbook_get_recipe", {"recipe_id": created_recipe_id}
)
@@ -84,16 +84,16 @@ async def test_mcp_cookbook_create_and_read_recipe(
assert read_recipe["description"] == "A test recipe created via MCP tools"
assert len(read_recipe["recipeIngredient"]) == 3
logger.info(f"Successfully verified recipe {created_recipe_id} via MCP")
logger.info("Successfully verified recipe %s via MCP", created_recipe_id)
finally:
# Cleanup
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_update_recipe(
@@ -115,11 +115,11 @@ async def test_mcp_cookbook_update_recipe(
try:
# 1. Create recipe via direct client
logger.info(f"Creating recipe for update test: {recipe_name}")
logger.info("Creating recipe for update test: %s", recipe_name)
created_recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
# 2. Update recipe via MCP (tool handles fetching current recipe internally)
logger.info(f"Updating recipe via MCP: {created_recipe_id}")
logger.info("Updating recipe via MCP: %s", created_recipe_id)
update_result = await nc_mcp_client.call_tool(
"nc_cookbook_update_recipe",
{
@@ -143,16 +143,16 @@ async def test_mcp_cookbook_update_recipe(
assert len(updated_recipe["recipeInstructions"]) == 2
assert updated_recipe["recipeCategory"] == "Updated"
logger.info(f"Successfully updated recipe {created_recipe_id} via MCP")
logger.info("Successfully updated recipe %s via MCP", created_recipe_id)
finally:
# Cleanup
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_delete_recipe(
@@ -173,11 +173,11 @@ async def test_mcp_cookbook_delete_recipe(
try:
# 1. Create recipe via direct client
logger.info(f"Creating recipe for delete test: {recipe_name}")
logger.info("Creating recipe for delete test: %s", recipe_name)
created_recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
# 2. Delete recipe via MCP
logger.info(f"Deleting recipe via MCP: {created_recipe_id}")
logger.info("Deleting recipe via MCP: %s", created_recipe_id)
delete_result = await nc_mcp_client.call_tool(
"nc_cookbook_delete_recipe", {"recipe_id": created_recipe_id}
)
@@ -192,7 +192,9 @@ async def test_mcp_cookbook_delete_recipe(
pytest.fail("Recipe should have been deleted but was still found")
except Exception:
# Expected - recipe should be deleted
logger.info(f"Successfully verified recipe {created_recipe_id} was deleted")
logger.info(
"Successfully verified recipe %s was deleted", created_recipe_id
)
created_recipe_id = None # Mark as cleaned up
finally:
@@ -200,9 +202,9 @@ async def test_mcp_cookbook_delete_recipe(
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_import_recipe_from_url(
@@ -221,7 +223,7 @@ async def test_mcp_cookbook_import_recipe_from_url(
try:
# 1. Import recipe via MCP
logger.info(f"Importing recipe from nginx container via MCP: {test_url}")
logger.info("Importing recipe from nginx container via MCP: %s", test_url)
import_result = await nc_mcp_client.call_tool(
"nc_cookbook_import_recipe", {"url": test_url}
)
@@ -234,7 +236,7 @@ async def test_mcp_cookbook_import_recipe_from_url(
created_recipe_id = int(import_response["recipe_id"])
imported_recipe = import_response["recipe"]
logger.info(f"Successfully imported recipe via MCP: {imported_recipe['name']}")
logger.info("Successfully imported recipe via MCP: %s", imported_recipe["name"])
# 2. Verify basic recipe structure
assert imported_recipe["name"] == "Black Pepper Tofu"
@@ -247,16 +249,16 @@ async def test_mcp_cookbook_import_recipe_from_url(
# 3. Verify we can read it back via direct NextcloudClient
retrieved = await nc_client.cookbook.get_recipe(created_recipe_id)
assert retrieved["name"] == imported_recipe["name"]
logger.info(f"Verified imported recipe ID: {created_recipe_id}")
logger.info("Verified imported recipe ID: %s", created_recipe_id)
finally:
# Cleanup
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up imported recipe {created_recipe_id}")
logger.info("Cleaned up imported recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup imported recipe: {e}")
logger.warning("Failed to cleanup imported recipe: %s", e)
async def test_mcp_cookbook_search_recipes(
@@ -278,14 +280,14 @@ async def test_mcp_cookbook_search_recipes(
try:
# 1. Create recipe via direct client
logger.info(f"Creating recipe for search test with keyword: {unique_keyword}")
logger.info("Creating recipe for search test with keyword: %s", unique_keyword)
created_recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
# 2. Allow time for indexing
await anyio.sleep(2)
# 3. Search for the recipe via MCP
logger.info(f"Searching for recipes via MCP with keyword: {unique_keyword}")
logger.info("Searching for recipes via MCP with keyword: %s", unique_keyword)
search_result = await nc_mcp_client.call_tool(
"nc_cookbook_search_recipes", {"query": unique_keyword}
)
@@ -304,7 +306,7 @@ async def test_mcp_cookbook_search_recipes(
found = any(str(r.get("id")) == str(created_recipe_id) for r in search_results)
assert found, f"Recipe {created_recipe_id} not found in search results"
logger.info(
f"Successfully found recipe {created_recipe_id} in MCP search results"
"Successfully found recipe %s in MCP search results", created_recipe_id
)
finally:
@@ -312,9 +314,9 @@ async def test_mcp_cookbook_search_recipes(
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_list_recipes(
@@ -333,7 +335,7 @@ async def test_mcp_cookbook_list_recipes(
recipes = list_response["recipes"]
assert isinstance(recipes, list)
logger.info(f"Found {len(recipes)} recipes via MCP")
logger.info("Found %s recipes via MCP", len(recipes))
async def test_mcp_cookbook_categories_workflow(
@@ -354,7 +356,7 @@ async def test_mcp_cookbook_categories_workflow(
try:
# 1. Create recipe in test category
logger.info(f"Creating recipe in category: {unique_category}")
logger.info("Creating recipe in category: %s", unique_category)
created_recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
# 2. Allow time for indexing
@@ -374,10 +376,10 @@ async def test_mcp_cookbook_categories_workflow(
categories = categories_response["categories"]
assert isinstance(categories, list)
logger.info(f"Found {len(categories)} categories via MCP")
logger.info("Found %s categories via MCP", len(categories))
# 4. Get recipes in this category via MCP
logger.info(f"Getting recipes in category via MCP: {unique_category}")
logger.info("Getting recipes in category via MCP: %s", unique_category)
category_recipes_result = await nc_mcp_client.call_tool(
"nc_cookbook_get_recipes_in_category", {"category": unique_category}
)
@@ -399,16 +401,16 @@ async def test_mcp_cookbook_categories_workflow(
assert found, (
f"Recipe {created_recipe_id} not found in category {unique_category}"
)
logger.info(f"Successfully found recipe in category {unique_category} via MCP")
logger.info("Successfully found recipe in category %s via MCP", unique_category)
finally:
# Cleanup
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_keywords_workflow(
@@ -429,7 +431,7 @@ async def test_mcp_cookbook_keywords_workflow(
try:
# 1. Create recipe with test keywords
logger.info(f"Creating recipe with keyword: {unique_keyword}")
logger.info("Creating recipe with keyword: %s", unique_keyword)
created_recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
# 2. Allow extra time for indexing and trigger reindex
@@ -449,10 +451,10 @@ async def test_mcp_cookbook_keywords_workflow(
keywords = keywords_response["keywords"]
assert isinstance(keywords, list)
logger.info(f"Found {len(keywords)} keywords via MCP")
logger.info("Found %s keywords via MCP", len(keywords))
# 4. Get recipes with this keyword via MCP
logger.info(f"Getting recipes with keyword via MCP: {unique_keyword}")
logger.info("Getting recipes with keyword via MCP: %s", unique_keyword)
keyword_recipes_result = await nc_mcp_client.call_tool(
"nc_cookbook_get_recipes_with_keywords", {"keywords": [unique_keyword]}
)
@@ -475,15 +477,17 @@ async def test_mcp_cookbook_keywords_workflow(
)
if found:
logger.info(
f"Successfully found recipe with keyword {unique_keyword} via MCP"
"Successfully found recipe with keyword %s via MCP", unique_keyword
)
else:
logger.warning(
f"Recipe {created_recipe_id} not in keyword results via MCP, but other recipes found"
"Recipe %s not in keyword results via MCP, but other recipes found",
created_recipe_id,
)
else:
logger.warning(
f"No recipes found with keyword {unique_keyword} via MCP - may be indexing delay"
"No recipes found with keyword %s via MCP - may be indexing delay",
unique_keyword,
)
finally:
@@ -491,9 +495,9 @@ async def test_mcp_cookbook_keywords_workflow(
if created_recipe_id is not None:
try:
await nc_client.cookbook.delete_recipe(created_recipe_id)
logger.info(f"Cleaned up recipe {created_recipe_id}")
logger.info("Cleaned up recipe %s", created_recipe_id)
except Exception as e:
logger.warning(f"Failed to cleanup recipe: {e}")
logger.warning("Failed to cleanup recipe: %s", e)
async def test_mcp_cookbook_config_and_version(
@@ -509,7 +513,7 @@ async def test_mcp_cookbook_config_and_version(
version_response = json.loads(version_result.contents[0].text)
assert "cookbook_version" in version_response
assert "api_version" in version_response
logger.info(f"Cookbook version from MCP: {version_response}")
logger.info("Cookbook version from MCP: %s", version_response)
# 2. Verify version via direct NextcloudClient
direct_version = await nc_client.cookbook.get_version()
@@ -526,7 +530,7 @@ async def test_mcp_cookbook_config_and_version(
assert len(config_result.contents) > 0
config_response = json.loads(config_result.contents[0].text)
assert isinstance(config_response, dict)
logger.info(f"Cookbook config from MCP: {config_response}")
logger.info("Cookbook config from MCP: %s", config_response)
# 4. Verify config via direct NextcloudClient
direct_config = await nc_client.cookbook.get_config()
@@ -551,4 +555,4 @@ async def test_mcp_cookbook_reindex(
reindex_response = json.loads(reindex_result.content[0].text)
assert isinstance(reindex_response["message"], str)
logger.info(f"Reindex result from MCP: {reindex_response['message']}")
logger.info("Reindex result from MCP: %s", reindex_response["message"])
+31 -31
View File
@@ -21,7 +21,7 @@ async def test_deck_stack_mcp_tools(
stack_order = 1
# 1. Create stack via MCP tool
logger.info(f"Creating stack via MCP: {stack_title}")
logger.info("Creating stack via MCP: %s", stack_title)
create_result = await nc_mcp_client.call_tool(
"deck_create_stack",
{"board_id": board_id, "title": stack_title, "order": stack_order},
@@ -34,11 +34,11 @@ async def test_deck_stack_mcp_tools(
stack_id = created_stack_response["id"]
assert created_stack_response["title"] == stack_title
assert created_stack_response["order"] == stack_order
logger.info(f"Stack created via MCP with ID: {stack_id}")
logger.info("Stack created via MCP with ID: %s", stack_id)
try:
# 2. Get stack via MCP resource
logger.info(f"Getting stack via MCP resource: {stack_id}")
logger.info("Getting stack via MCP resource: %s", stack_id)
get_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks/{stack_id}"
)
@@ -51,7 +51,7 @@ async def test_deck_stack_mcp_tools(
# 3. Update stack via MCP tool
updated_title = f"Updated {stack_title}"
updated_order = 2
logger.info(f"Updating stack via MCP tool: {stack_id}")
logger.info("Updating stack via MCP tool: %s", stack_id)
update_result = await nc_mcp_client.call_tool(
"deck_update_stack",
{
@@ -86,10 +86,10 @@ async def test_deck_stack_mcp_tools(
# Verify our stack is in the list
stack_ids = [stack["id"] for stack in stacks_data]
assert stack_id in stack_ids, "Updated stack not found in list"
logger.info(f"Stack {stack_id} found in stacks list")
logger.info("Stack %s found in stacks list", stack_id)
# 6. Read stack via MCP resource
logger.info(f"Reading stack via MCP resource: {stack_id}")
logger.info("Reading stack via MCP resource: %s", stack_id)
read_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks/{stack_id}"
)
@@ -100,7 +100,7 @@ async def test_deck_stack_mcp_tools(
finally:
# Clean up
await nc_client.deck.delete_stack(board_id, stack_id)
logger.info(f"Cleaned up stack ID: {stack_id}")
logger.info("Cleaned up stack ID: %s", stack_id)
# Card MCP Tools Tests
@@ -117,7 +117,7 @@ async def test_deck_card_mcp_tools(
card_description = f"Test description for {card_title}"
# 1. Create card via MCP tool
logger.info(f"Creating card via MCP: {card_title}")
logger.info("Creating card via MCP: %s", card_title)
create_result = await nc_mcp_client.call_tool(
"deck_create_card",
{
@@ -137,11 +137,11 @@ async def test_deck_card_mcp_tools(
card_id = created_card_response["id"]
assert created_card_response["title"] == card_title
assert created_card_response["description"] == card_description
logger.info(f"Card created via MCP with ID: {card_id}")
logger.info("Card created via MCP with ID: %s", card_id)
try:
# 2. Get card via MCP resource
logger.info(f"Getting card via MCP resource: {card_id}")
logger.info("Getting card via MCP resource: %s", card_id)
get_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}"
)
@@ -154,7 +154,7 @@ async def test_deck_card_mcp_tools(
# 3. Update card via MCP tool
updated_title = f"Updated {card_title}"
updated_description = f"Updated description for {card_title}"
logger.info(f"Updating card via MCP tool: {card_id}")
logger.info("Updating card via MCP tool: %s", card_id)
update_result = await nc_mcp_client.call_tool(
"deck_update_card",
{
@@ -178,7 +178,7 @@ async def test_deck_card_mcp_tools(
logger.info("Card update verified via direct client")
# 5. Archive/unarchive card via MCP tools
logger.info(f"Archiving card via MCP tool: {card_id}")
logger.info("Archiving card via MCP tool: %s", card_id)
archive_result = await nc_mcp_client.call_tool(
"deck_archive_card",
{"board_id": board_id, "stack_id": stack_id, "card_id": card_id},
@@ -189,7 +189,7 @@ async def test_deck_card_mcp_tools(
)
logger.info("Card archived via MCP tool successfully")
logger.info(f"Unarchiving card via MCP tool: {card_id}")
logger.info("Unarchiving card via MCP tool: %s", card_id)
unarchive_result = await nc_mcp_client.call_tool(
"deck_unarchive_card",
{"board_id": board_id, "stack_id": stack_id, "card_id": card_id},
@@ -201,7 +201,7 @@ async def test_deck_card_mcp_tools(
logger.info("Card unarchived via MCP tool successfully")
# 6. Move card to different position via MCP tool
logger.info(f"Reordering card via MCP tool: {card_id}")
logger.info("Reordering card via MCP tool: %s", card_id)
reorder_result = await nc_mcp_client.call_tool(
"deck_reorder_card",
{
@@ -219,7 +219,7 @@ async def test_deck_card_mcp_tools(
logger.info("Card reordered via MCP tool successfully")
# 7. Read card via MCP resource
logger.info(f"Reading card via MCP resource: {card_id}")
logger.info("Reading card via MCP resource: %s", card_id)
read_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}"
)
@@ -230,7 +230,7 @@ async def test_deck_card_mcp_tools(
finally:
# Clean up
await nc_client.deck.delete_card(board_id, stack_id, card_id)
logger.info(f"Cleaned up card ID: {card_id}")
logger.info("Cleaned up card ID: %s", card_id)
# Label MCP Tools Tests
@@ -243,7 +243,7 @@ async def test_deck_label_mcp_tools(
label_color = "FF0000" # Red
# 1. Create label via MCP tool
logger.info(f"Creating label via MCP: {label_title}")
logger.info("Creating label via MCP: %s", label_title)
create_result = await nc_mcp_client.call_tool(
"deck_create_label",
{"board_id": board_id, "title": label_title, "color": label_color},
@@ -256,11 +256,11 @@ async def test_deck_label_mcp_tools(
label_id = created_label_response["id"]
assert created_label_response["title"] == label_title
assert created_label_response["color"] == label_color
logger.info(f"Label created via MCP with ID: {label_id}")
logger.info("Label created via MCP with ID: %s", label_id)
try:
# 2. Get label via MCP resource
logger.info(f"Getting label via MCP resource: {label_id}")
logger.info("Getting label via MCP resource: %s", label_id)
get_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/labels/{label_id}"
)
@@ -273,7 +273,7 @@ async def test_deck_label_mcp_tools(
# 3. Update label via MCP tool
updated_title = f"Updated {label_title}"
updated_color = "00FF00" # Green
logger.info(f"Updating label via MCP tool: {label_id}")
logger.info("Updating label via MCP tool: %s", label_id)
update_result = await nc_mcp_client.call_tool(
"deck_update_label",
{
@@ -296,7 +296,7 @@ async def test_deck_label_mcp_tools(
logger.info("Label update verified via direct client")
# 5. Read label via MCP resource
logger.info(f"Reading label via MCP resource: {label_id}")
logger.info("Reading label via MCP resource: %s", label_id)
read_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/labels/{label_id}"
)
@@ -307,7 +307,7 @@ async def test_deck_label_mcp_tools(
finally:
# Clean up
await nc_client.deck.delete_label(board_id, label_id)
logger.info(f"Cleaned up label ID: {label_id}")
logger.info("Cleaned up label ID: %s", label_id)
# Label-Card Assignment Tests
@@ -330,7 +330,7 @@ async def test_deck_card_label_assignment_mcp_tools(
try:
# 1. Assign label to card via MCP tool
logger.info(f"Assigning label {label_id} to card {card_id} via MCP")
logger.info("Assigning label %s to card %s via MCP", label_id, card_id)
assign_result = await nc_mcp_client.call_tool(
"deck_assign_label_to_card",
{
@@ -354,7 +354,7 @@ async def test_deck_card_label_assignment_mcp_tools(
logger.info("Label assignment verified via direct client")
# 3. Remove label from card via MCP tool
logger.info(f"Removing label {label_id} from card {card_id} via MCP")
logger.info("Removing label %s from card %s via MCP", label_id, card_id)
remove_result = await nc_mcp_client.call_tool(
"deck_remove_label_from_card",
{
@@ -382,7 +382,7 @@ async def test_deck_card_label_assignment_mcp_tools(
finally:
# Clean up
await nc_client.deck.delete_label(board_id, label_id)
logger.info(f"Cleaned up label ID: {label_id}")
logger.info("Cleaned up label ID: %s", label_id)
# User Assignment Tests
@@ -401,7 +401,7 @@ async def test_deck_card_user_assignment_mcp_tools(
user_id = "admin"
# 1. Assign user to card via MCP tool
logger.info(f"Assigning user {user_id} to card {card_id} via MCP")
logger.info("Assigning user %s to card %s via MCP", user_id, card_id)
assign_result = await nc_mcp_client.call_tool(
"deck_assign_user_to_card",
{
@@ -432,7 +432,7 @@ async def test_deck_card_user_assignment_mcp_tools(
logger.info("User assignment verified via direct client")
# 3. Unassign user from card via MCP tool
logger.info(f"Unassigning user {user_id} from card {card_id} via MCP")
logger.info("Unassigning user %s from card %s via MCP", user_id, card_id)
unassign_result = await nc_mcp_client.call_tool(
"deck_unassign_user_from_card",
{
@@ -521,7 +521,7 @@ async def test_deck_mcp_resource_templates(nc_mcp_client: ClientSession):
assert expected_template in template_uris, (
f"Expected template '{expected_template}' not found"
)
logger.info(f"Found expected deck resource template: {expected_template}")
logger.info("Found expected deck resource template: %s", expected_template)
# Listing resource tests
@@ -534,7 +534,7 @@ async def test_deck_mcp_listing_resources(
stack_id = stack_data["id"]
# 1. Test listing stacks resource
logger.info(f"Reading stacks list via MCP resource for board {board_id}")
logger.info("Reading stacks list via MCP resource for board %s", board_id)
stacks_resource_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks"
)
@@ -547,7 +547,7 @@ async def test_deck_mcp_listing_resources(
logger.info("Stack found in stacks resource list")
# 2. Test listing cards resource
logger.info(f"Reading cards list via MCP resource for stack {stack_id}")
logger.info("Reading cards list via MCP resource for stack %s", stack_id)
cards_resource_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/stacks/{stack_id}/cards"
)
@@ -560,7 +560,7 @@ async def test_deck_mcp_listing_resources(
logger.info("Card found in cards resource list")
# 3. Test listing labels resource
logger.info(f"Reading labels list via MCP resource for board {board_id}")
logger.info("Reading labels list via MCP resource for board %s", board_id)
labels_resource_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{board_id}/labels"
)
+15 -15
View File
@@ -25,7 +25,7 @@ async def test_deck_mcp_connectivity(nc_mcp_client: ClientSession):
assert expected_tool in tool_names, (
f"Expected deck tool '{expected_tool}' not found in available tools"
)
logger.info(f"Found expected deck tool: {expected_tool}")
logger.info("Found expected deck tool: %s", expected_tool)
# List available resource templates
templates = await nc_mcp_client.list_resource_templates()
@@ -40,7 +40,7 @@ async def test_deck_mcp_connectivity(nc_mcp_client: ClientSession):
assert expected_template in template_uris, (
f"Expected deck template '{expected_template}' not found"
)
logger.info(f"Found expected deck resource template: {expected_template}")
logger.info("Found expected deck resource template: %s", expected_template)
# List available resources
resources = await nc_mcp_client.list_resources()
@@ -55,7 +55,7 @@ async def test_deck_mcp_connectivity(nc_mcp_client: ClientSession):
assert expected_resource in resource_uris, (
f"Expected deck resource '{expected_resource}' not found"
)
logger.info(f"Found expected deck resource: {expected_resource}")
logger.info("Found expected deck resource: %s", expected_resource)
async def test_deck_board_crud_workflow_mcp(
@@ -68,7 +68,7 @@ async def test_deck_board_crud_workflow_mcp(
board_color = "0000FF" # Blue
# 1. Create board via MCP
logger.info(f"Creating board via MCP: {board_title}")
logger.info("Creating board via MCP: %s", board_title)
create_result = await nc_mcp_client.call_tool(
"deck_create_board",
{"title": board_title, "color": board_color},
@@ -81,7 +81,7 @@ async def test_deck_board_crud_workflow_mcp(
created_board_response = json.loads(created_board_json)
board_id = created_board_response["id"]
logger.info(f"Board created via MCP with ID: {board_id}")
logger.info("Board created via MCP with ID: %s", board_id)
assert created_board_response["title"] == board_title
assert created_board_response["color"] == board_color
@@ -94,7 +94,7 @@ async def test_deck_board_crud_workflow_mcp(
logger.info("Board creation verified via direct client")
# 3. Read board via MCP resource
logger.info(f"Reading board via MCP resource: {board_id}")
logger.info("Reading board via MCP resource: %s", board_id)
read_result = await nc_mcp_client.read_resource(f"nc://Deck/boards/{board_id}")
assert len(read_result.contents) == 1, "Expected exactly one content item"
read_board_data = json.loads(read_result.contents[0].text)
@@ -104,7 +104,7 @@ async def test_deck_board_crud_workflow_mcp(
logger.info("Board read via MCP resource successfully")
# 4. Verify board via direct read of resource
logger.info(f"Verifying board via resource read: {board_id}")
logger.info("Verifying board via resource read: %s", board_id)
# This was already done in step 3, so we'll just log confirmation
logger.info("Board structure verified successfully")
@@ -124,7 +124,7 @@ async def test_deck_board_crud_workflow_mcp(
# Clean up - delete board
await nc_client.deck.delete_board(board_id)
logger.info(f"Cleaned up board ID: {board_id}")
logger.info("Cleaned up board ID: %s", board_id)
async def test_deck_board_operations_error_handling_mcp(nc_mcp_client: ClientSession):
@@ -143,7 +143,7 @@ async def test_deck_board_operations_error_handling_mcp(nc_mcp_client: ClientSes
logger.info("Invalid board creation correctly failed via MCP tool")
# Test read non-existent board via MCP resource
logger.info(f"Testing read non-existent board via MCP resource: {non_existent_id}")
logger.info("Testing read non-existent board via MCP resource: %s", non_existent_id)
try:
read_result = await nc_mcp_client.read_resource(
f"nc://Deck/boards/{non_existent_id}"
@@ -153,7 +153,7 @@ async def test_deck_board_operations_error_handling_mcp(nc_mcp_client: ClientSes
"Expected empty content for non-existent board"
)
except Exception as e:
logger.info(f"Read non-existent board correctly failed via MCP resource: {e}")
logger.info("Read non-existent board correctly failed via MCP resource: %s", e)
async def test_deck_board_creation_validation_mcp(nc_mcp_client: ClientSession):
@@ -185,11 +185,11 @@ async def test_deck_board_creation_success_mcp(
assert create_result.isError is False, "Valid board creation should succeed"
created_board = json.loads(create_result.content[0].text)
board_id = created_board["id"]
logger.info(f"Valid board created successfully with ID: {board_id}")
logger.info("Valid board created successfully with ID: %s", board_id)
# Clean up - delete board
await nc_client.deck.delete_board(board_id)
logger.info(f"Cleaned up board ID: {board_id}")
logger.info("Cleaned up board ID: %s", board_id)
async def test_deck_workflow_integration_mcp(
@@ -202,7 +202,7 @@ async def test_deck_workflow_integration_mcp(
board_title = board_data["title"]
# 1. Read board via MCP to verify the structure
logger.info(f"Reading board via MCP resource: {board_id}")
logger.info("Reading board via MCP resource: %s", board_id)
read_result = await nc_mcp_client.read_resource(f"nc://Deck/boards/{board_id}")
board_mcp_data = json.loads(read_result.contents[0].text)
@@ -219,7 +219,7 @@ async def test_deck_workflow_integration_mcp(
logger.info("Board found in boards list")
# 3. Verify board data matches via resource (already done in step 1)
logger.info(f"Board data verification completed for board: {board_id}")
logger.info("Board data verification completed for board: %s", board_id)
logger.info("Board structure and data verified successfully")
@@ -250,7 +250,7 @@ async def test_deck_card_comment_crud_workflow_mcp(
assert comment["objectId"] == card_id
assert comment["message"] == "Initial comment"
assert comment["replyTo"] is None
logger.info(f"Created comment ID {comment_id} on card {card_id}")
logger.info("Created comment ID %s on card %s", comment_id, card_id)
# 2. List comments via MCP — verify the new comment is present
list_result = await nc_mcp_client.call_tool(
+5 -5
View File
@@ -40,7 +40,7 @@ async def test_search_with_empty_query(nc_mcp_client: ClientSession):
# Search with empty query
response = await nc_mcp_client.call_tool("nc_notes_search_notes", {"query": ""})
logger.info(f"Empty search query response: {response}")
logger.info("Empty search query response: %s", response)
# Should return successful response with empty or valid results
assert response is not None
@@ -54,7 +54,7 @@ async def test_tool_missing_required_parameters(nc_mcp_client: ClientSession):
"nc_notes_create_note",
{"title": "Test"}, # Missing content and category
)
logger.info(f"Missing params response: {response}")
logger.info("Missing params response: %s", response)
# Should return error response for missing required parameters
assert response is not None
@@ -108,7 +108,7 @@ async def test_calendar_missing_calendar_error(nc_mcp_client: ClientSession):
},
)
logger.info(f"Non-existent calendar response: {response}")
logger.info("Non-existent calendar response: %s", response)
# Should return structured error response
assert response is not None
@@ -131,7 +131,7 @@ async def test_webdav_read_missing_file_error(nc_mcp_client: ClientSession):
"nc_webdav_read_file", {"path": "non-existent-file.txt"}
)
logger.info(f"Missing file response: {response}")
logger.info("Missing file response: %s", response)
# Should return structured error response
assert response is not None
@@ -154,7 +154,7 @@ async def test_tables_missing_table_error(nc_mcp_client: ClientSession):
"nc_tables_get_schema", {"table_id": 999999}
)
logger.info(f"Missing table response: {response}")
logger.info("Missing table response: %s", response)
# Should return structured error response
assert response is not None
+42 -42
View File
@@ -19,7 +19,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("Available MCP tools:")
tool_names = []
for tool in tools.tools:
logger.info(f" - {tool.name}: {tool.description}")
logger.info(" - %s: %s", tool.name, tool.description)
tool_names.append(tool.name)
# Verify expected tools are present
@@ -88,7 +88,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("\nAvailable resource templates:")
template_uris = []
for template in templates.resourceTemplates:
logger.info(f" - {template.uriTemplate}")
logger.info(" - %s", template.uriTemplate)
template_uris.append(template.uriTemplate)
# Verify expected resource templates
@@ -105,7 +105,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("\nAvailable resources:")
resource_uris = []
for resource in resources.resources:
logger.info(f" - {resource.uri}: {resource.name}")
logger.info(" - %s: %s", resource.uri, resource.name)
resource_uris.append(str(resource.uri)) # Convert to string for comparison
# Verify expected resources
@@ -126,7 +126,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
prompts = await nc_mcp_client.list_prompts()
logger.info("\nAvailable prompts:")
for prompt in prompts.prompts:
logger.info(f" - {prompt.name}")
logger.info(" - %s", prompt.name)
async def test_mcp_notes_crud_workflow(
@@ -143,7 +143,7 @@ async def test_mcp_notes_crud_workflow(
try:
# 1. Create note via MCP
logger.info(f"Creating note via MCP: {test_title}")
logger.info("Creating note via MCP: %s", test_title)
create_result = await nc_mcp_client.call_tool(
"nc_notes_create_note",
{"title": test_title, "content": test_content, "category": test_category},
@@ -157,7 +157,7 @@ async def test_mcp_notes_crud_workflow(
note_id = note_data["id"]
create_etag = note_data["etag"] # Verify create response includes ETag
logger.info(f"Note created via MCP with ID: {note_id}, ETag: {create_etag}")
logger.info("Note created via MCP with ID: %s, ETag: %s", note_id, create_etag)
assert "etag" in note_data, "Create response should include ETag"
assert create_etag, "Create ETag should not be empty"
@@ -170,7 +170,7 @@ async def test_mcp_notes_crud_workflow(
assert direct_note["category"] == test_category, "Category mismatch"
# 3. Read note via MCP
logger.info(f"Reading note via MCP: {note_id}")
logger.info("Reading note via MCP: %s", note_id)
read_result = await nc_mcp_client.call_tool(
"nc_notes_get_note", {"note_id": note_id}
)
@@ -186,7 +186,7 @@ async def test_mcp_notes_crud_workflow(
updated_content = f"Updated content: {test_content}"
etag = read_note_data["etag"]
logger.info(f"Updating note via MCP: {note_id}")
logger.info("Updating note via MCP: %s", note_id)
update_result = await nc_mcp_client.call_tool(
"nc_notes_update_note",
{
@@ -206,7 +206,7 @@ async def test_mcp_notes_crud_workflow(
updated_note_data = json.loads(update_result.content[0].text)
update_etag = updated_note_data["etag"]
logger.info(f"Note updated via MCP, new ETag: {update_etag}")
logger.info("Note updated via MCP, new ETag: %s", update_etag)
assert "etag" in updated_note_data, "Update response should include ETag"
assert update_etag, "Update ETag should not be empty"
assert update_etag != etag, "ETag should change after update"
@@ -218,7 +218,7 @@ async def test_mcp_notes_crud_workflow(
# 6. Append content via MCP
append_content = "\n\nThis is appended content via MCP."
logger.info(f"Appending content to note via MCP: {note_id}")
logger.info("Appending content to note via MCP: %s", note_id)
append_result = await nc_mcp_client.call_tool(
"nc_notes_append_content", {"note_id": note_id, "content": append_content}
)
@@ -231,7 +231,7 @@ async def test_mcp_notes_crud_workflow(
appended_note_data = json.loads(append_result.content[0].text)
append_etag = appended_note_data["etag"]
logger.info(f"Content appended via MCP, new ETag: {append_etag}")
logger.info("Content appended via MCP, new ETag: %s", append_etag)
assert "etag" in appended_note_data, "Append response should include ETag"
assert append_etag, "Append ETag should not be empty"
assert append_etag != update_etag, "ETag should change after append"
@@ -241,7 +241,7 @@ async def test_mcp_notes_crud_workflow(
assert append_content in appended_direct_note["content"]
# 8. Search for note via MCP
logger.info(f"Searching for note via MCP with query: {unique_suffix}")
logger.info("Searching for note via MCP with query: %s", unique_suffix)
search_result = await nc_mcp_client.call_tool(
"nc_notes_search_notes", {"query": unique_suffix}
)
@@ -250,7 +250,7 @@ async def test_mcp_notes_crud_workflow(
f"MCP note search failed: {search_result.content}"
)
search_notes_text = search_result.content[0].text
logger.info(f"Search result text: {search_notes_text}")
logger.info("Search result text: %s", search_notes_text)
search_response = json.loads(search_notes_text)
# Expect structured response with Pydantic format
@@ -282,7 +282,7 @@ async def test_mcp_notes_crud_workflow(
assert found_note["title"] == updated_title
# 9. Delete note via MCP
logger.info(f"Deleting note via MCP: {note_id}")
logger.info("Deleting note via MCP: %s", note_id)
delete_result = await nc_mcp_client.call_tool(
"nc_notes_delete_note", {"note_id": note_id}
)
@@ -297,7 +297,7 @@ async def test_mcp_notes_crud_workflow(
pytest.fail("Note should have been deleted but was still found")
except Exception:
# Expected - note should be deleted
logger.info(f"Successfully verified note {note_id} was deleted")
logger.info("Successfully verified note %s was deleted", note_id)
created_note = None # Mark as cleaned up
finally:
@@ -306,9 +306,9 @@ async def test_mcp_notes_crud_workflow(
try:
note_data = json.loads(created_note)
await nc_client.notes.delete_note(note_data["id"])
logger.info(f"Cleaned up note {note_data['id']} after test failure")
logger.info("Cleaned up note %s after test failure", note_data["id"])
except Exception as e:
logger.warning(f"Failed to cleanup note: {e}")
logger.warning("Failed to cleanup note: %s", e)
async def test_mcp_notes_etag_conflict(
@@ -325,7 +325,7 @@ async def test_mcp_notes_etag_conflict(
try:
# 1. Create note via MCP
logger.info(f"Creating note for ETag conflict test: {test_title}")
logger.info("Creating note for ETag conflict test: %s", test_title)
create_result = await nc_mcp_client.call_tool(
"nc_notes_create_note",
{"title": test_title, "content": test_content, "category": test_category},
@@ -355,7 +355,7 @@ async def test_mcp_notes_etag_conflict(
assert new_etag != original_etag, "ETag should have changed after update"
# 3. Try to update with the stale (original) ETag - this should fail
logger.info(f"Attempting update with stale ETag: {original_etag}")
logger.info("Attempting update with stale ETag: %s", original_etag)
conflict_result = await nc_mcp_client.call_tool(
"nc_notes_update_note",
{
@@ -382,9 +382,9 @@ async def test_mcp_notes_etag_conflict(
if created_note is not None:
try:
await nc_client.notes.delete_note(created_note["id"])
logger.info(f"Cleaned up test note {created_note['id']}")
logger.info("Cleaned up test note %s", created_note["id"])
except Exception as e:
logger.warning(f"Failed to cleanup test note: {e}")
logger.warning("Failed to cleanup test note: %s", e)
async def test_mcp_webdav_workflow(
@@ -400,7 +400,7 @@ async def test_mcp_webdav_workflow(
try:
# 1. Create directory via MCP
logger.info(f"Creating directory via MCP: {test_dir}")
logger.info("Creating directory via MCP: %s", test_dir)
create_dir_result = await nc_mcp_client.call_tool(
"nc_webdav_create_directory", {"path": test_dir}
)
@@ -415,7 +415,7 @@ async def test_mcp_webdav_workflow(
assert test_dir in dir_names, f"Directory {test_dir} not found in root listing"
# 3. Write file via MCP
logger.info(f"Writing file via MCP: {test_file_path}")
logger.info("Writing file via MCP: %s", test_file_path)
write_result = await nc_mcp_client.call_tool(
"nc_webdav_write_file",
{
@@ -437,7 +437,7 @@ async def test_mcp_webdav_workflow(
)
# 5. Read file via MCP
logger.info(f"Reading file via MCP: {test_file_path}")
logger.info("Reading file via MCP: %s", test_file_path)
read_result = await nc_mcp_client.call_tool(
"nc_webdav_read_file", {"path": test_file_path}
)
@@ -458,7 +458,7 @@ async def test_mcp_webdav_workflow(
assert direct_content.decode("utf-8") == test_content
# 7. List directory via MCP
logger.info(f"Listing directory via MCP: {test_dir}")
logger.info("Listing directory via MCP: %s", test_dir)
list_result = await nc_mcp_client.call_tool(
"nc_webdav_list_directory", {"path": test_dir}
)
@@ -467,7 +467,7 @@ async def test_mcp_webdav_workflow(
f"MCP directory listing failed: {list_result.content}"
)
listing_text = list_result.content[0].text
logger.info(f"Directory listing response: {listing_text}")
logger.info("Directory listing response: %s", listing_text)
listing_data = json.loads(listing_text)
# Extract files from DirectoryListing response
@@ -498,17 +498,17 @@ async def test_mcp_webdav_workflow(
finally:
# Cleanup
try:
logger.info(f"Cleaning up test file: {test_file_path}")
logger.info("Cleaning up test file: %s", test_file_path)
await nc_mcp_client.call_tool(
"nc_webdav_delete_resource", {"path": test_file_path}
)
logger.info(f"Cleaning up test directory: {test_dir}")
logger.info("Cleaning up test directory: %s", test_dir)
await nc_mcp_client.call_tool(
"nc_webdav_delete_resource", {"path": test_dir}
)
except Exception as e:
logger.warning(f"Failed to cleanup WebDAV resources: {e}")
logger.warning("Failed to cleanup WebDAV resources: %s", e)
async def test_mcp_resources_access(
@@ -576,8 +576,8 @@ async def test_mcp_calendar_workflow(
calendars_response = json.loads(calendars_result.content[0].text)
# Debug output to understand the structure
logger.info(f"calendars_response type: {type(calendars_response)}")
logger.info(f"calendars_response content: {calendars_response}")
logger.info("calendars_response type: %s", type(calendars_response))
logger.info("calendars_response content: %s", calendars_response)
# Expect structured response with Pydantic format
assert isinstance(calendars_response, dict), (
@@ -600,7 +600,7 @@ async def test_mcp_calendar_workflow(
# Use the first available calendar
calendar_name = calendars_list[0]["name"]
logger.info(f"Using calendar: {calendar_name}")
logger.info("Using calendar: %s", calendar_name)
# 2. Create event via MCP
from datetime import datetime, timedelta
@@ -621,7 +621,7 @@ async def test_mcp_calendar_workflow(
"priority": 5,
}
logger.info(f"Creating event via MCP: {test_event_title}")
logger.info("Creating event via MCP: %s", test_event_title)
create_result = await nc_mcp_client.call_tool(
"nc_calendar_create_event", event_data
)
@@ -634,7 +634,7 @@ async def test_mcp_calendar_workflow(
event_uid = created_event_data["uid"]
created_event = {"uid": event_uid, "calendar_name": calendar_name}
logger.info(f"Event created via MCP with UID: {event_uid}")
logger.info("Event created via MCP with UID: %s", event_uid)
# 3. Verify creation via direct NextcloudClient
direct_event, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
@@ -643,7 +643,7 @@ async def test_mcp_calendar_workflow(
assert "testing" in direct_event.get("categories", "")
# 4. Get event via MCP
logger.info(f"Getting event via MCP: {event_uid}")
logger.info("Getting event via MCP: %s", event_uid)
get_result = await nc_mcp_client.call_tool(
"nc_calendar_get_event",
{"calendar_name": calendar_name, "event_uid": event_uid},
@@ -686,8 +686,8 @@ async def test_mcp_calendar_workflow(
events_response = json.loads(list_result.content[0].text)
# Debug output to understand what nc_calendar_list_events returns
logger.info(f"list_events result type: {type(events_response)}")
logger.info(f"list_events result content: {events_response}")
logger.info("list_events result type: %s", type(events_response))
logger.info("list_events result content: %s", events_response)
# Response is now a ListEventsResponse with an "events" field
assert isinstance(events_response, dict), "Expected response dict"
@@ -748,7 +748,7 @@ async def test_mcp_calendar_workflow(
"priority": 1,
}
logger.info(f"Updating event via MCP: {event_uid}")
logger.info("Updating event via MCP: %s", event_uid)
update_result = await nc_mcp_client.call_tool(
"nc_calendar_update_event", update_data
)
@@ -784,7 +784,7 @@ async def test_mcp_calendar_workflow(
assert isinstance(upcoming_events, list), "Expected upcoming events list"
# 10. Delete event via MCP
logger.info(f"Deleting event via MCP: {event_uid}")
logger.info("Deleting event via MCP: %s", event_uid)
delete_result = await nc_mcp_client.call_tool(
"nc_calendar_delete_event",
{"calendar_name": calendar_name, "event_uid": event_uid},
@@ -800,7 +800,7 @@ async def test_mcp_calendar_workflow(
pytest.fail("Event should have been deleted but was still found")
except Exception:
# Expected - event should be deleted
logger.info(f"Successfully verified event {event_uid} was deleted")
logger.info("Successfully verified event %s was deleted", event_uid)
created_event = None # Mark as cleaned up
except Exception as e:
@@ -818,7 +818,7 @@ async def test_mcp_calendar_workflow(
created_event["calendar_name"], created_event["uid"]
)
logger.info(
f"Cleaned up event {created_event['uid']} after test failure"
"Cleaned up event %s after test failure", created_event["uid"]
)
except Exception as e:
logger.warning(f"Failed to cleanup event: {e}")
logger.warning("Failed to cleanup event: %s", e)
+1 -1
View File
@@ -53,7 +53,7 @@ async def test_talk_send_and_read_workflow(
assert posted["message"] == "Hello from MCP integration test"
assert posted["token"] == token
posted_id = posted["id"]
logger.info(f"Posted message id={posted_id} into token={token}")
logger.info("Posted message id=%s into token=%s", posted_id, token)
# 2. Cross-check via direct client
direct_messages, _ = await nc_client.talk.get_messages(token, limit=10)
+11 -11
View File
@@ -53,16 +53,16 @@ async def search_test_files(nc_client: NextcloudClient):
for file_path, content, content_type in test_files:
await nc_client.webdav.write_file(file_path, content, content_type)
logger.info(f"Created {len(test_files)} test files in {test_dir}")
logger.info("Created %s test files in %s", len(test_files), test_dir)
yield test_dir
# Cleanup
try:
await nc_client.webdav.delete_resource(test_dir)
logger.info(f"Cleaned up test directory: {test_dir}")
logger.info("Cleaned up test directory: %s", test_dir)
except Exception as e:
logger.warning(f"Failed to cleanup {test_dir}: {e}")
logger.warning("Failed to cleanup %s: %s", test_dir, e)
async def test_nc_webdav_find_by_name(
@@ -82,7 +82,7 @@ async def test_nc_webdav_find_by_name(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} files matching 'search_%.txt'")
logger.info("Found %s files matching 'search_%%.txt'", len(files))
# Should find at least 3 .txt files
assert len(files) >= 3, f"Expected at least 3 .txt files, got {len(files)}"
@@ -113,7 +113,7 @@ async def test_nc_webdav_find_by_name_with_limit(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} files with limit=2")
logger.info("Found %s files with limit=2", len(files))
# Should return at most 2 results
assert len(files) <= 2, f"Expected at most 2 files, got {len(files)}"
@@ -136,7 +136,7 @@ async def test_nc_webdav_find_by_type_images(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} image files")
logger.info("Found %s image files", len(files))
# Should find at least 2 image files (jpg and png)
assert len(files) >= 2, f"Expected at least 2 image files, got {len(files)}"
@@ -165,7 +165,7 @@ async def test_nc_webdav_find_by_type_specific(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} PDF files")
logger.info("Found %s PDF files", len(files))
# Should find at least 1 PDF
assert len(files) >= 1, f"Expected at least 1 PDF file, got {len(files)}"
@@ -194,7 +194,7 @@ async def test_nc_webdav_search_files_basic(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} markdown files")
logger.info("Found %s markdown files", len(files))
# Should find at least 2 .md files
assert len(files) >= 2, f"Expected at least 2 .md files, got {len(files)}"
@@ -222,7 +222,7 @@ async def test_nc_webdav_search_files_combined(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} files matching combined filters")
logger.info("Found %s files matching combined filters", len(files))
# Should find search_test1.txt and search_test2.txt
assert len(files) >= 2, f"Expected at least 2 files, got {len(files)}"
@@ -255,7 +255,7 @@ async def test_nc_webdav_search_files_with_limit(
content = result.content[0].text
files = normalize_search_response(json.loads(content))
logger.info(f"Found {len(files)} files with limit=3")
logger.info("Found %s files with limit=3", len(files))
# Should return at most 3 results
assert len(files) <= 3, f"Expected at most 3 files, got {len(files)}"
@@ -318,5 +318,5 @@ async def test_search_result_properties(
extended_props = ["file_id", "etag", "size", "content_type", "last_modified"]
present_props = [prop for prop in extended_props if prop in file]
logger.info(f"Search result properties: {list(file.keys())}")
logger.info("Search result properties: %s", list(file.keys()))
assert len(present_props) > 0, f"Should have at least one of {extended_props}"