Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,6 @@ Thumbs.db

# Playwright auth state (contains session cookies/tokens)
.playwright_auth_state.json

# test resources
testing/
18 changes: 18 additions & 0 deletions campus_python/api/v1/timetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Campus API timetable resource (v1).
"""

import typing

import campus.model

from ...interface import Resource, ResourceCollection
Expand Down Expand Up @@ -114,6 +116,22 @@ def new(self, metadata: dict, data: dict) -> dict:
resp.raise_for_status()
return resp.json()

def list(self, **filters: typing.Any) -> "list[campus.model.TimetableMetadata]":
"""List timetables matching the provided filters.

Args:
**filters: Arbitrary filter parameters applied to the query.

Returns:
list[campus.model.TimetableMetadata]: Matching timetable metadata objects.
"""
resp = self.client.get(self.make_path(), query=filters if filters else None)
resp.raise_for_status()
return [
campus.model.TimetableMetadata.from_resource(item)
for item in resp.json()
]

class Timetable(Resource):
"""A single timetable with start date."""

Expand Down
162 changes: 162 additions & 0 deletions tests/unit/test_timetable_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import unittest
from unittest.mock import Mock, MagicMock

from campus_python.api.v1.timetable import Timetables
from campus_python.interface import ResourceRoot
import campus.model


class TestTimetablesList(unittest.TestCase):
"""Test Timetables.list() method."""

def setUp(self):
"""Set up test fixtures."""
# Create mock client
self.mock_client = Mock()
self.mock_client.base_url = "https://api.example.com"

# Create resource root
self.root = ResourceRoot(self.mock_client)
self.root.url_prefix = "api/v1"

# Create Timetables instance
self.timetables = Timetables(self.mock_client, root=self.root)

def test_list_without_filters(self):
"""Test list() returns timetables without filters."""
# Mock the response - backend returns direct list
mock_response = Mock()
mock_response.json.return_value = [
{
"id": "tt-123",
"filename": "schedule.xlsx",
"start_date": "2026-01-01T00:00:00Z",
"end_date": "2026-06-30T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
},
{
"id": "tt-456",
"filename": "other.xlsx",
"start_date": "2026-07-01T00:00:00Z",
"end_date": "2026-12-31T00:00:00Z",
"created_at": "2026-07-01T00:00:00Z"
}
]
mock_response.raise_for_status = Mock()

self.mock_client.get.return_value = mock_response

# Call list()
result = self.timetables.list()

# Verify the client was called correctly
self.mock_client.get.assert_called_once_with(
"/api/v1/timetable/",
query=None
)

# Verify results
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "tt-123")
self.assertEqual(result[0].filename, "schedule.xlsx")
self.assertEqual(result[1].id, "tt-456")
self.assertEqual(result[1].filename, "other.xlsx")

def test_list_with_filters(self):
"""Test list() with filter parameters."""
# Mock the response - backend returns direct list
mock_response = Mock()
mock_response.json.return_value = [
{
"id": "tt-123",
"filename": "schedule.xlsx",
"start_date": "2026-01-01T00:00:00Z",
"end_date": "2026-06-30T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
}
]
mock_response.raise_for_status = Mock()

self.mock_client.get.return_value = mock_response

# Call list() with filters
result = self.timetables.list(filename="schedule.xlsx")

# Verify the client was called with correct filters
self.mock_client.get.assert_called_once_with(
"/api/v1/timetable/",
query={"filename": "schedule.xlsx"}
)

# Verify results
self.assertEqual(len(result), 1)
self.assertEqual(result[0].filename, "schedule.xlsx")

def test_list_empty_result(self):
"""Test list() returns empty list when no timetables found."""
# Mock the response - backend returns direct list
mock_response = Mock()
mock_response.json.return_value = []
mock_response.raise_for_status = Mock()

self.mock_client.get.return_value = mock_response

# Call list()
result = self.timetables.list()

# Verify results
self.assertEqual(len(result), 0)

def test_list_with_multiple_filters(self):
"""Test list() with multiple filter parameters."""
# Mock the response - backend returns direct list
mock_response = Mock()
mock_response.json.return_value = [
{
"id": "tt-789",
"filename": "test.xlsx",
"start_date": "2026-01-01T00:00:00Z",
"end_date": "2026-06-30T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
}
]
mock_response.raise_for_status = Mock()

self.mock_client.get.return_value = mock_response

# Call list() with multiple filters
result = self.timetables.list(
filename="test.xlsx",
start_date="2026-01-01T00:00:00Z"
)

# Verify the client was called with correct filters
self.mock_client.get.assert_called_once_with(
"/api/v1/timetable/",
query={
"filename": "test.xlsx",
"start_date": "2026-01-01T00:00:00Z"
}
)

# Verify results
self.assertEqual(len(result), 1)
self.assertEqual(result[0].filename, "test.xlsx")

def test_list_propagates_client_errors(self):
"""Test list() propagates client errors."""
# Mock response with raise_for_status that raises exception
mock_response = Mock()
mock_response.raise_for_status.side_effect = Exception("Client error")

self.mock_client.get.return_value = mock_response

# Call list() should raise exception
with self.assertRaises(Exception) as context:
self.timetables.list()

self.assertEqual(str(context.exception), "Client error")


if __name__ == "__main__":
unittest.main()
Loading