#!/usr/bin/env python3 """ Unit tests for calendar Flask endpoints in api/app.py. Covers: GET /api/calendar/users POST /api/calendar/users DELETE /api/calendar/users/ POST /api/calendar/calendars POST /api/calendar/events GET /api/calendar/events// GET /api/calendar/status GET /api/calendar/connectivity """ import sys import json import unittest from pathlib import Path from unittest.mock import patch, MagicMock api_dir = Path(__file__).parent.parent / 'api' sys.path.insert(0, str(api_dir)) from app import app class TestGetCalendarUsers(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_get_users_returns_200_with_list(self, mock_cm): mock_cm.get_users.return_value = [ {'username': 'alice', 'email': 'alice@cell'}, {'username': 'bob', 'email': 'bob@cell'}, ] r = self.client.get('/api/calendar/users') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIsInstance(data, list) self.assertEqual(len(data), 2) @patch('app.calendar_manager') def test_get_users_returns_200_with_empty_list(self, mock_cm): mock_cm.get_users.return_value = [] r = self.client.get('/api/calendar/users') self.assertEqual(r.status_code, 200) self.assertEqual(json.loads(r.data), []) @patch('app.calendar_manager') def test_get_users_returns_500_on_exception(self, mock_cm): mock_cm.get_users.side_effect = Exception('radicale unreachable') r = self.client.get('/api/calendar/users') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestCreateCalendarUser(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_create_user_returns_200_on_valid_body(self, mock_cm): mock_cm.create_calendar_user.return_value = True r = self.client.post( '/api/calendar/users', data=json.dumps({'username': 'alice', 'password': 'secret123'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('created', data) @patch('app.calendar_manager') def test_create_user_passes_credentials_to_manager(self, mock_cm): mock_cm.create_calendar_user.return_value = True self.client.post( '/api/calendar/users', data=json.dumps({'username': 'alice', 'password': 'secret123'}), content_type='application/json', ) mock_cm.create_calendar_user.assert_called_once_with('alice', 'secret123') @patch('app.calendar_manager') def test_create_user_returns_400_when_no_body(self, mock_cm): r = self.client.post('/api/calendar/users') self.assertEqual(r.status_code, 400) data = json.loads(r.data) self.assertIn('error', data) mock_cm.create_calendar_user.assert_not_called() @patch('app.calendar_manager') def test_create_user_returns_400_when_username_missing(self, mock_cm): r = self.client.post( '/api/calendar/users', data=json.dumps({'password': 'secret123'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_cm.create_calendar_user.assert_not_called() @patch('app.calendar_manager') def test_create_user_returns_400_when_password_missing(self, mock_cm): r = self.client.post( '/api/calendar/users', data=json.dumps({'username': 'alice'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_cm.create_calendar_user.assert_not_called() @patch('app.calendar_manager') def test_create_user_returns_500_on_exception(self, mock_cm): mock_cm.create_calendar_user.side_effect = Exception('htpasswd write failure') r = self.client.post( '/api/calendar/users', data=json.dumps({'username': 'alice', 'password': 'secret123'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestDeleteCalendarUser(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_delete_user_returns_200_on_success(self, mock_cm): mock_cm.delete_calendar_user.return_value = True r = self.client.delete('/api/calendar/users/alice') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('deleted', data) @patch('app.calendar_manager') def test_delete_user_passes_username_to_manager(self, mock_cm): mock_cm.delete_calendar_user.return_value = True self.client.delete('/api/calendar/users/bob') mock_cm.delete_calendar_user.assert_called_once_with('bob') @patch('app.calendar_manager') def test_delete_user_returns_500_on_exception(self, mock_cm): mock_cm.delete_calendar_user.side_effect = Exception('user not found') r = self.client.delete('/api/calendar/users/alice') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestCreateCalendar(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_create_calendar_returns_200_on_valid_body(self, mock_cm): mock_cm.create_calendar.return_value = True r = self.client.post( '/api/calendar/calendars', data=json.dumps({'username': 'alice', 'name': 'Work'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('created', data) @patch('app.calendar_manager') def test_create_calendar_accepts_calendar_name_alias(self, mock_cm): mock_cm.create_calendar.return_value = True r = self.client.post( '/api/calendar/calendars', data=json.dumps({'username': 'alice', 'calendar_name': 'Personal'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) @patch('app.calendar_manager') def test_create_calendar_returns_400_when_no_body(self, mock_cm): r = self.client.post('/api/calendar/calendars') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_cm.create_calendar.assert_not_called() @patch('app.calendar_manager') def test_create_calendar_returns_400_when_username_missing(self, mock_cm): r = self.client.post( '/api/calendar/calendars', data=json.dumps({'name': 'Work'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.calendar_manager') def test_create_calendar_returns_400_when_name_missing(self, mock_cm): r = self.client.post( '/api/calendar/calendars', data=json.dumps({'username': 'alice'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.calendar_manager') def test_create_calendar_returns_500_on_exception(self, mock_cm): mock_cm.create_calendar.side_effect = Exception('CalDAV error') r = self.client.post( '/api/calendar/calendars', data=json.dumps({'username': 'alice', 'name': 'Work'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddCalendarEvent(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_add_event_returns_200_on_valid_body(self, mock_cm): mock_cm.add_event.return_value = 'event-uid-123' r = self.client.post( '/api/calendar/events', data=json.dumps({ 'username': 'alice', 'calendar_name': 'Work', 'summary': 'Team Meeting', 'dtstart': '20260427T100000Z', }), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('created', data) @patch('app.calendar_manager') def test_add_event_returns_400_when_no_body(self, mock_cm): r = self.client.post('/api/calendar/events') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_cm.add_event.assert_not_called() @patch('app.calendar_manager') def test_add_event_returns_400_when_username_missing(self, mock_cm): r = self.client.post( '/api/calendar/events', data=json.dumps({'calendar_name': 'Work', 'summary': 'Meeting'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.calendar_manager') def test_add_event_returns_400_when_calendar_missing(self, mock_cm): r = self.client.post( '/api/calendar/events', data=json.dumps({'username': 'alice', 'summary': 'Meeting'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.calendar_manager') def test_add_event_returns_500_on_exception(self, mock_cm): mock_cm.add_event.side_effect = Exception('iCalendar parse error') r = self.client.post( '/api/calendar/events', data=json.dumps({ 'username': 'alice', 'calendar_name': 'Work', 'summary': 'Meeting', }), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestGetCalendarEvents(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_get_events_returns_200_with_events(self, mock_cm): mock_cm.get_events.return_value = [ {'uid': 'abc', 'summary': 'Standup', 'dtstart': '20260427T090000Z'}, ] r = self.client.get('/api/calendar/events/alice/Work') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIsInstance(data, list) self.assertEqual(len(data), 1) @patch('app.calendar_manager') def test_get_events_passes_username_and_calendar_to_manager(self, mock_cm): mock_cm.get_events.return_value = [] self.client.get('/api/calendar/events/bob/Personal') mock_cm.get_events.assert_called_once() args = mock_cm.get_events.call_args[0] self.assertEqual(args[0], 'bob') self.assertEqual(args[1], 'Personal') @patch('app.calendar_manager') def test_get_events_returns_500_on_exception(self, mock_cm): mock_cm.get_events.side_effect = Exception('calendar not found') r = self.client.get('/api/calendar/events/alice/Work') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestGetCalendarStatus(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_get_status_returns_200_with_status_dict(self, mock_cm): mock_cm.get_status.return_value = { 'running': True, 'port': 5232, 'users_count': 3, } r = self.client.get('/api/calendar/status') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('running', data) @patch('app.calendar_manager') def test_get_status_returns_500_on_exception(self, mock_cm): mock_cm.get_status.side_effect = Exception('container not found') r = self.client.get('/api/calendar/status') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestCalendarConnectivity(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.calendar_manager') def test_connectivity_returns_200_with_result(self, mock_cm): mock_cm.test_connectivity.return_value = { 'caldav': True, 'carddav': True, 'latency_ms': 8, } r = self.client.get('/api/calendar/connectivity') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('caldav', data) @patch('app.calendar_manager') def test_connectivity_returns_500_on_exception(self, mock_cm): mock_cm.test_connectivity.side_effect = Exception('connection refused') r = self.client.get('/api/calendar/connectivity') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) if __name__ == '__main__': unittest.main()