# Copyright The IETF Trust 2021, All Rights Reserved # -*- coding: utf-8 -*- import datetime import requests import requests_mock from pytz import timezone, utc from unittest.mock import patch from urllib.parse import urljoin from django.conf import settings from django.test import override_settings from ietf.utils.tests import TestCase from .meetecho import Conference, ConferenceManager, MeetechoAPI, MeetechoAPIError API_BASE = 'https://meetecho-api.example.com' CLIENT_ID = 'datatracker' CLIENT_SECRET = 'very secret' API_CONFIG={ 'api_base': API_BASE, 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, } @override_settings(MEETECHO_API_CONFIG=API_CONFIG) class APITests(TestCase): retrieve_token_url = urljoin(API_BASE, 'auth/ietfservice/tokens') schedule_meeting_url = urljoin(API_BASE, 'meeting/interim/createRoom') fetch_meetings_url = urljoin(API_BASE, 'meeting/interim/fetchRooms') delete_meetings_url = urljoin(API_BASE, 'meeting/interim/deleteRoom') def setUp(self): super().setUp() self.requests_mock = requests_mock.Mocker() self.requests_mock.start() def tearDown(self): self.requests_mock.stop() super().tearDown() def test_retrieve_wg_tokens(self): data_to_fetch = { 'tokens': { 'acro': 'wg-token-value-for-acro', 'beta': 'different-token', 'gamma': 'this-is-not-the-same', } } self.requests_mock.post(self.retrieve_token_url, status_code=200, json=data_to_fetch) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.retrieve_wg_tokens(['acro', 'beta', 'gamma']) self.assertTrue(self.requests_mock.called) request = self.requests_mock.last_request self.assertEqual( request.headers['Content-Type'], 'application/json', 'Incorrect request content-type', ) self.assertEqual( request.json(), { 'client': CLIENT_ID, 'secret': CLIENT_SECRET, 'wgs': ['acro', 'beta', 'gamma'], } ) self.assertEqual(api_response, data_to_fetch) def test_schedule_meeting(self): self.requests_mock.post( self.schedule_meeting_url, status_code=200, json={ 'rooms': { '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { 'id': 18, 'start_time': '2021-09-14 10:00:00', 'duration': 130, 'description': 'interim-2021-wgname-01', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32', 'deletion_token': 'session-deletion-token', }, } }, ) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.schedule_meeting( wg_token='my-token', start_time=utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), duration=datetime.timedelta(minutes=130), description='interim-2021-wgname-01', extrainfo='message for staff', ) self.assertTrue(self.requests_mock.called) request = self.requests_mock.last_request self.assertIn('Authorization', request.headers) self.assertEqual( request.headers['Content-Type'], 'application/json', 'Incorrect request content-type', ) self.assertEqual(request.headers['Authorization'], 'bearer my-token', 'Incorrect request authorization header') self.assertEqual( request.json(), { 'duration': 130, 'start_time': '2021-09-14 10:00:00', 'extrainfo': 'message for staff', 'description': 'interim-2021-wgname-01', }, 'Incorrect request content' ) # same time in different time zones for start_time in [ utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)), timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)), timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)), timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)), ]: self.assertEqual( api_response, { 'rooms': { '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { 'id': 18, 'start_time': start_time, 'duration': datetime.timedelta(minutes=130), 'description': 'interim-2021-wgname-01', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32', 'deletion_token': 'session-deletion-token', }, } }, f'Incorrect time conversion for {start_time.tzinfo.zone}', ) def test_fetch_meetings(self): self.maxDiff = 2048 self.requests_mock.get( self.fetch_meetings_url, status_code=200, json={ 'rooms': { '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { 'id': 18, 'start_time': '2021-09-14 10:00:00', 'duration': 130, 'description': 'interim-2021-wgname-01', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32', 'deletion_token': 'session-deletion-token-01', }, 'e68e96d4-d38f-475b-9073-ecab46ca96a5': { 'room': { 'id': 23, 'start_time': '2021-09-15 14:30:00', 'duration': 30, 'description': 'interim-2021-wgname-02', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=e68e96d4-d38f-475b-9073-ecab46ca96a5', 'deletion_token': 'session-deletion-token-02', }, } }, ) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.fetch_meetings(wg_token='my-token') self.assertTrue(self.requests_mock.called) request = self.requests_mock.last_request self.assertIn('Authorization', request.headers) self.assertEqual(request.headers['Authorization'], 'bearer my-token', 'Incorrect request authorization header') self.assertEqual( api_response, { 'rooms': { '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { 'id': 18, 'start_time': utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), 'duration': datetime.timedelta(minutes=130), 'description': 'interim-2021-wgname-01', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32', 'deletion_token': 'session-deletion-token-01', }, 'e68e96d4-d38f-475b-9073-ecab46ca96a5': { 'room': { 'id': 23, 'start_time': utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)), 'duration': datetime.timedelta(minutes=30), 'description': 'interim-2021-wgname-02', }, 'url': 'https://meetings.conf.meetecho.com/interim/?short=e68e96d4-d38f-475b-9073-ecab46ca96a5', 'deletion_token': 'session-deletion-token-02', }, } }, ) def test_delete_meeting(self): data_to_fetch = {} self.requests_mock.post(self.delete_meetings_url, status_code=200, json=data_to_fetch) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.delete_meeting(deletion_token='delete-this-meeting-please') self.assertTrue(self.requests_mock.called) request = self.requests_mock.last_request self.assertIn('Authorization', request.headers) self.assertEqual(request.headers['Authorization'], 'bearer delete-this-meeting-please', 'Incorrect request authorization header') self.assertIsNone(request.body, 'Delete meeting request has no body') self.assertCountEqual(api_response, data_to_fetch) def test_request_helper_failed_requests(self): self.requests_mock.register_uri(requests_mock.ANY, urljoin(API_BASE, 'unauthorized/url/endpoint'), status_code=401) self.requests_mock.register_uri(requests_mock.ANY, urljoin(API_BASE, 'forbidden/url/endpoint'), status_code=403) self.requests_mock.register_uri(requests_mock.ANY, urljoin(API_BASE, 'notfound/url/endpoint'), status_code=404) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) for method in ['POST', 'GET']: for code, endpoint in ((401, 'unauthorized/url/endpoint'), (403, 'forbidden/url/endpoint'), (404, 'notfound/url/endpoint')): with self.assertRaises(Exception) as context: api._request(method, endpoint) self.assertIsInstance(context.exception, MeetechoAPIError) self.assertIn(str(code), str(context.exception)) def test_request_helper_exception(self): self.requests_mock.register_uri(requests_mock.ANY, urljoin(API_BASE, 'exception/url/endpoint'), exc=requests.exceptions.RequestException) api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) for method in ['POST', 'GET']: with self.assertRaises(Exception) as context: api._request(method, 'exception/url/endpoint') self.assertIsInstance(context.exception, MeetechoAPIError) def test_time_serialization(self): """Time de/serialization should be consistent""" time = datetime.datetime.now(utc).replace(microsecond=0) # cut off to 0 microseconds api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) self.assertEqual(api._deserialize_time(api._serialize_time(time)), time) @override_settings(MEETECHO_API_CONFIG=API_CONFIG) class ConferenceManagerTests(TestCase): def test_conference_from_api_dict(self): confs = Conference.from_api_dict( None, { 'session-1-uuid': { 'room': { 'id': 1, 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, 'url': 'https://example.com/some/url', 'deletion_token': 'delete-me', }, 'session-2-uuid': { 'room': { 'id': 2, 'start_time': utc.localize(datetime.datetime(2022,2,5,4,5,6)), 'duration': datetime.timedelta(minutes=90), 'description': 'another-description', }, 'url': 'https://example.com/another/url', 'deletion_token': 'delete-me-too', }, } ) self.assertCountEqual( confs, [ Conference( manager=None, id=1, public_id='session-1-uuid', description='some-description', start_time=utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', ), Conference( manager=None, id=2, public_id='session-2-uuid', description='another-description', start_time=utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)), duration=datetime.timedelta(minutes=90), url='https://example.com/another/url', deletion_token='delete-me-too', ), ] ) @patch.object(ConferenceManager, 'wg_token', return_value='atoken') @patch('ietf.utils.meetecho.MeetechoAPI.fetch_meetings') def test_fetch(self, mock_fetch, _): mock_fetch.return_value = { 'rooms': { 'session-1-uuid': { 'room': { 'id': 1, 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, 'url': 'https://example.com/some/url', 'deletion_token': 'delete-me', }, } } cm = ConferenceManager(settings.MEETECHO_API_CONFIG) fetched = cm.fetch('acronym') self.assertEqual( fetched, [Conference( manager=cm, id=1, public_id='session-1-uuid', description='some-description', start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', )], ) self.assertEqual(mock_fetch.call_args[0], ('atoken',)) @patch.object(ConferenceManager, 'wg_token', return_value='atoken') @patch('ietf.utils.meetecho.MeetechoAPI.schedule_meeting') def test_create(self, mock_schedule, _): mock_schedule.return_value = { 'rooms': { 'session-1-uuid': { 'room': { 'id': 1, 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, 'url': 'https://example.com/some/url', 'deletion_token': 'delete-me', }, }, } cm = ConferenceManager(settings.MEETECHO_API_CONFIG) result = cm.create('group', 'desc', 'starttime', 'dur', 'extra') self.assertEqual( result, [Conference( manager=cm, id=1, public_id='session-1-uuid', description='some-description', start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', )] ) args, kwargs = mock_schedule.call_args self.assertEqual( kwargs, { 'wg_token': 'atoken', 'description': 'desc', 'start_time': 'starttime', 'duration': 'dur', 'extrainfo': 'extra', }) @patch('ietf.utils.meetecho.MeetechoAPI.delete_meeting') def test_delete_conference(self, mock_delete): cm = ConferenceManager(settings.MEETECHO_API_CONFIG) cm.delete_conference(Conference(None, None, None, None, None, None, None, 'delete-this')) args, kwargs = mock_delete.call_args self.assertEqual(args, ('delete-this',)) @patch('ietf.utils.meetecho.MeetechoAPI.delete_meeting') def test_delete_by_url(self, mock_delete): cm = ConferenceManager(settings.MEETECHO_API_CONFIG) cm.delete_conference(Conference(None, None, None, None, None, None, 'the-url', 'delete-this')) args, kwargs = mock_delete.call_args self.assertEqual(args, ('delete-this',))