We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sweeden-ttu/canvas-lms-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Canvas LMS Live API Tests
These tests hit the REAL Canvas API at Texas Tech University.
They are designed to:
1. Verify endpoint accessibility with current credentials
2. Discover which endpoints work for the current user
3. Skip tests gracefully when required IDs are missing
Run with: uv run pytest tests/test_canvas_live.py -v
Note: Some tests may be skipped if test_hints.json is not configured.
"""
import pytest
import httpx
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import get_config, get_api_headers
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture(scope="module")
def canvas_config():
"""Load Canvas configuration."""
config, hints = get_config()
return config, hints
@pytest.fixture(scope="module")
def api_client(canvas_config):
"""Create an HTTP client for Canvas API."""
config, _ = canvas_config
client = httpx.Client(
base_url=config.base_url,
headers=get_api_headers(config.api_token),
timeout=30.0,
)
yield client
client.close()
@pytest.fixture(scope="module")
def course_id(canvas_config):
"""Get a valid course ID from hints, or skip if not available."""
_, hints = canvas_config
if not hints.valid_course_ids:
pytest.skip(
"Missing 'valid_course_ids' in test_hints.json. "
"Please add at least one course ID to continue. "
"You can find course IDs in the URL when viewing a course in Canvas "
"(e.g., https://texastech.instructure.com/courses/58606)"
)
return hints.valid_course_ids[0]
@pytest.fixture(scope="module")
def all_course_ids(canvas_config):
"""Get all valid course IDs from hints."""
_, hints = canvas_config
if not hints.valid_course_ids:
pytest.skip("Missing 'valid_course_ids' in test_hints.json")
return hints.valid_course_ids
@pytest.fixture(scope="module")
def module_id(api_client, course_id):
"""Get a valid module ID from the first course, or skip if not available."""
response = api_client.get(
f"/api/v1/courses/{course_id}/modules",
params={"per_page": 10}
)
if response.status_code == 200:
modules = response.json()
if modules:
return modules[0]["id"]
pytest.skip(f"No modules found in course {course_id}")
@pytest.fixture(scope="module")
def assignment_id(api_client, course_id):
"""Get a valid assignment ID from the first course, or skip if not available."""
response = api_client.get(
f"/api/v1/courses/{course_id}/assignments",
params={"per_page": 10}
)
if response.status_code == 200:
assignments = response.json()
if assignments:
return assignments[0]["id"]
pytest.skip(f"No assignments found in course {course_id}")
@pytest.fixture(scope="module")
def file_id(api_client, course_id, module_id):
"""Get a valid file ID from module items, or skip if not available."""
response = api_client.get(
f"/api/v1/courses/{course_id}/modules/{module_id}/items",
params={"per_page": 50}
)
if response.status_code == 200:
items = response.json()
for item in items:
if item.get("type") == "File" and item.get("content_id"):
return item["content_id"]
pytest.skip(f"No files found in module {module_id} of course {course_id}")
# =============================================================================
# Discovery Tests (No IDs Required)
# =============================================================================
class TestDiscoveryEndpoints:
"""Tests for endpoints that don't require any IDs."""
def test_user_profile(self, api_client):
"""GET /api/v1/users/self/profile - Get current user profile."""
response = api_client.get("/api/v1/users/self/profile")
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert "id" in data, "Response should contain user ID"
assert "name" in data, "Response should contain user name"
print(f"\n ✓ Authenticated as: {data.get('name', 'Unknown')}")
print(f" ✓ User ID: {data.get('id')}")
print(f" ✓ Login ID: {data.get('login_id', 'N/A')}")
def test_list_courses(self, api_client):
"""GET /api/v1/courses - List enrolled courses."""
response = api_client.get(
"/api/v1/courses",
params={"enrollment_state": "active", "per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} active course(s)")
for course in data[:5]: # Show first 5
print(f" - {course.get('id')}: {course.get('name', 'Unnamed')}")
if len(data) > 5:
print(f" ... and {len(data) - 5} more")
def test_todo_items(self, api_client):
"""GET /api/v1/users/self/todo - Get to-do items."""
response = api_client.get(
"/api/v1/users/self/todo",
params={"per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} to-do item(s)")
def test_upcoming_events(self, api_client):
"""GET /api/v1/users/self/upcoming_events - Get upcoming events."""
response = api_client.get(
"/api/v1/users/self/upcoming_events",
params={"per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} upcoming event(s)")
def test_calendar_events(self, api_client):
"""GET /api/v1/calendar_events - List calendar events."""
response = api_client.get(
"/api/v1/calendar_events",
params={"per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} calendar event(s)")
def test_planner_items(self, api_client):
"""GET /api/v1/planner/items - List planner items."""
response = api_client.get(
"/api/v1/planner/items",
params={"per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} planner item(s)")
def test_planner_notes(self, api_client):
"""GET /api/v1/planner_notes - List planner notes."""
response = api_client.get(
"/api/v1/planner_notes",
params={"per_page": 50}
)
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} planner note(s)")
# =============================================================================
# Deep-Dive Tests (Course ID Required)
# =============================================================================
class TestCourseEndpoints:
"""Tests for course-specific endpoints. Requires valid_course_id in test_hints.json."""
def test_course_assignments(self, api_client, course_id):
"""GET /api/v1/courses/{id}/assignments - Get course assignments."""
response = api_client.get(
f"/api/v1/courses/{course_id}/assignments",
params={"per_page": 50, "order_by": "due_at"}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}. "
f"Course ID {course_id} may be invalid or you may not have access."
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} assignment(s) in course {course_id}")
for assignment in data[:3]:
due = assignment.get('due_at', 'No due date')
print(f" - {assignment.get('name', 'Unnamed')}: due {due}")
def test_course_modules(self, api_client, course_id):
"""GET /api/v1/courses/{id}/modules - Get course modules."""
response = api_client.get(
f"/api/v1/courses/{course_id}/modules",
params={"per_page": 50}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}. "
f"Course {course_id} may not have modules or you may lack access."
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} module(s) in course {course_id}")
def test_discussion_topics(self, api_client, course_id):
"""GET /api/v1/courses/{id}/discussion_topics - Get discussions."""
response = api_client.get(
f"/api/v1/courses/{course_id}/discussion_topics",
params={"per_page": 50}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} discussion topic(s) in course {course_id}")
def test_enrollments_grades(self, api_client, course_id):
"""GET /api/v1/courses/{id}/enrollments - Get grades via enrollment."""
response = api_client.get(
f"/api/v1/courses/{course_id}/enrollments",
params={"user_id": "self", "type[]": "StudentEnrollment"}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} enrollment(s) in course {course_id}")
for enrollment in data:
grades = enrollment.get('grades', {})
if grades:
print(f" - Current grade: {grades.get('current_grade', 'N/A')}")
print(f" - Current score: {grades.get('current_score', 'N/A')}")
def test_announcements(self, api_client, all_course_ids):
"""GET /api/v1/announcements - Get announcements for courses."""
context_codes = [f"course_{cid}" for cid in all_course_ids]
response = api_client.get(
"/api/v1/announcements",
params={"context_codes[]": context_codes, "per_page": 50}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} announcement(s) across {len(all_course_ids)} course(s)")
def test_module_items(self, api_client, course_id, module_id):
"""GET /api/v1/courses/{id}/modules/{module_id}/items - Get module items."""
response = api_client.get(
f"/api/v1/courses/{course_id}/modules/{module_id}/items",
params={"per_page": 50}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} item(s) in module {module_id}")
file_count = sum(1 for item in data if item.get("type") == "File")
if file_count > 0:
print(f" - {file_count} file(s) found in module")
def test_course_pages(self, api_client, course_id):
"""GET /api/v1/courses/{id}/pages - Get course wiki pages."""
response = api_client.get(
f"/api/v1/courses/{course_id}/pages",
params={"per_page": 50}
)
if response.status_code == 200:
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} page(s) in course {course_id}")
elif response.status_code == 404:
print(f"\n ✓ Course {course_id} does not have wiki pages (404 - expected)")
pytest.skip("Course does not have wiki pages enabled")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_course_quizzes(self, api_client, course_id):
"""GET /api/v1/courses/{id}/quizzes - Get course quizzes."""
response = api_client.get(
f"/api/v1/courses/{course_id}/quizzes",
params={"per_page": 50}
)
if response.status_code == 200:
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} quiz(zes) in course {course_id}")
elif response.status_code == 404:
print(f"\n ✓ Course {course_id} does not have quizzes (404 - expected)")
pytest.skip("Course does not have quizzes or endpoint not available")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_course_sections(self, api_client, course_id):
"""GET /api/v1/courses/{id}/sections - Get course sections."""
response = api_client.get(
f"/api/v1/courses/{course_id}/sections",
params={"per_page": 50}
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} section(s) in course {course_id}")
def test_course_settings(self, api_client, course_id):
"""GET /api/v1/courses/{id}/settings - Get course settings."""
response = api_client.get(
f"/api/v1/courses/{course_id}/settings"
)
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}"
)
data = response.json()
assert isinstance(data, dict), "Response should be a dict"
print(f"\n ✓ Retrieved settings for course {course_id}")
def test_submissions(self, api_client, course_id, assignment_id):
"""GET /api/v1/courses/{id}/assignments/{assignment_id}/submissions - Get submissions."""
response = api_client.get(
f"/api/v1/courses/{course_id}/assignments/{assignment_id}/submissions",
params={"user_id": "self", "per_page": 10}
)
if response.status_code == 200:
data = response.json()
assert isinstance(data, list), "Response should be a list"
print(f"\n ✓ Found {len(data)} submission(s) for assignment {assignment_id}")
elif response.status_code == 403:
print(f"\n ✓ Submissions endpoint returns 403 (may require different permissions)")
pytest.skip("Submissions endpoint returns 403 - may require instructor permissions or different parameters")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_file_metadata(self, api_client, course_id, file_id):
"""GET /api/v1/courses/{id}/files/{file_id} - Get file metadata."""
response = api_client.get(
f"/api/v1/courses/{course_id}/files/{file_id}"
)
if response.status_code == 200:
data = response.json()
print(f"\n ✓ Retrieved file metadata for file {file_id}")
print(f" - File name: {data.get('filename', 'N/A')}")
print(f" - File size: {data.get('size', 'N/A')} bytes")
elif response.status_code == 403:
print(f"\n ✓ File endpoint returns 403 (expected for some files)")
pytest.skip("File access returns 403 - may require special permissions")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_file_public_url(self, api_client, file_id):
"""GET /api/v1/files/{file_id}/public_url - Get file public download URL."""
response = api_client.get(
f"/api/v1/files/{file_id}/public_url"
)
if response.status_code == 200:
data = response.json()
print(f"\n ✓ Retrieved public URL for file {file_id}")
assert "public_url" in data or "url" in data, "Response should contain URL"
elif response.status_code == 403:
print(f"\n ✓ File public URL endpoint returns 403 (expected for some files)")
pytest.skip("File public URL access returns 403 - may require special permissions")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_files_expect_403(self, api_client, course_id):
"""
GET /api/v1/courses/{id}/files - Expected to fail with 403.
This test documents that the /files endpoint is NOT accessible
for student accounts. This is expected behavior.
"""
response = api_client.get(
f"/api/v1/courses/{course_id}/files",
params={"per_page": 50}
)
# We expect this to fail for student accounts
if response.status_code == 403:
print(f"\n ✓ Files endpoint correctly returns 403 (expected for students)")
pytest.skip("Files endpoint returns 403 as expected - this is normal for student accounts")
elif response.status_code == 200:
# Unexpected success - maybe they have special permissions
print(f"\n ! Files endpoint returned 200 (unexpected - user may have elevated permissions)")
data = response.json()
print(f" Found {len(data)} file(s)")
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
# =============================================================================
# Error Handling Tests
# =============================================================================
class TestErrorHandling:
"""Tests for error scenarios."""
def test_invalid_course_id(self, api_client):
"""Verify 404 is returned for invalid course ID."""
response = api_client.get("/api/v1/courses/999999999/assignments")
# Canvas may return 404 or 401 depending on settings
assert response.status_code in [401, 403, 404], (
f"Expected 401/403/404 for invalid course, got {response.status_code}"
)
print(f"\n ✓ Invalid course ID correctly returns {response.status_code}")
def test_rate_limit_headers(self, api_client):
"""Check for rate limit headers in response."""
response = api_client.get("/api/v1/users/self/profile")
# Canvas includes rate limit info in headers
remaining = response.headers.get("X-Rate-Limit-Remaining")
if remaining:
print(f"\n ✓ Rate limit remaining: {remaining}")
else:
print("\n ℹ Rate limit headers not present in response")
# =============================================================================
# Integration Test
# =============================================================================
class TestIntegration:
"""End-to-end integration tests."""
def test_full_workflow(self, api_client, canvas_config):
"""Test a complete workflow: list courses -> get assignments."""
# Step 1: Get courses
courses_response = api_client.get(
"/api/v1/courses",
params={"enrollment_state": "active", "per_page": 10}
)
assert courses_response.status_code == 200
courses = courses_response.json()
if not courses:
pytest.skip("No courses available for integration test")
# Step 2: Get assignments from first course
course_id = courses[0]["id"]
assignments_response = api_client.get(
f"/api/v1/courses/{course_id}/assignments",
params={"per_page": 5}
)
assert assignments_response.status_code == 200
print(f"\n ✓ Integration test passed:")
print(f" - Listed {len(courses)} courses")
print(f" - Retrieved assignments from course {course_id}")
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])