Projet

Général

Profil

0001-add-connector-for-BigBlueButton-66156.patch

Benjamin Dauvergne, 15 juin 2022 18:05

Télécharger (52,4 ko)

Voir les différences:

Subject: [PATCH] add connector for BigBlueButton (#66156)

 passerelle/apps/bbb/__init__.py               |   0
 .../apps/bbb/migrations/0001_initial.py       | 112 ++++
 passerelle/apps/bbb/migrations/__init__.py    |   0
 passerelle/apps/bbb/models.py                 | 346 +++++++++++
 .../bbb/templates/bbb/resource_detail.html    |   2 +
 passerelle/apps/bbb/utils.py                  | 287 +++++++++
 passerelle/settings.py                        |   1 +
 tests/test_bbb.py                             | 549 ++++++++++++++++++
 8 files changed, 1297 insertions(+)
 create mode 100644 passerelle/apps/bbb/__init__.py
 create mode 100644 passerelle/apps/bbb/migrations/0001_initial.py
 create mode 100644 passerelle/apps/bbb/migrations/__init__.py
 create mode 100644 passerelle/apps/bbb/models.py
 create mode 100644 passerelle/apps/bbb/templates/bbb/resource_detail.html
 create mode 100644 passerelle/apps/bbb/utils.py
 create mode 100644 tests/test_bbb.py
passerelle/apps/bbb/migrations/0001_initial.py
1
# Generated by Django 2.2.28 on 2022-06-15 08:22
2

  
3
import uuid
4

  
5
import django.db.models.deletion
6
from django.db import migrations, models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    initial = True
12

  
13
    dependencies = [
14
        ('base', '0029_auto_20210202_1627'),
15
    ]
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='Resource',
20
            fields=[
21
                (
22
                    'id',
23
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
24
                ),
25
                ('title', models.CharField(max_length=50, verbose_name='Title')),
26
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
27
                ('description', models.TextField(verbose_name='Description')),
28
                (
29
                    'basic_auth_username',
30
                    models.CharField(
31
                        blank=True, max_length=128, verbose_name='Basic authentication username'
32
                    ),
33
                ),
34
                (
35
                    'basic_auth_password',
36
                    models.CharField(
37
                        blank=True, max_length=128, verbose_name='Basic authentication password'
38
                    ),
39
                ),
40
                (
41
                    'client_certificate',
42
                    models.FileField(
43
                        blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
44
                    ),
45
                ),
46
                (
47
                    'trusted_certificate_authorities',
48
                    models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'),
49
                ),
50
                (
51
                    'verify_cert',
52
                    models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'),
53
                ),
54
                (
55
                    'http_proxy',
56
                    models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
57
                ),
58
                (
59
                    'bbb_url',
60
                    models.URLField(
61
                        help_text='Base URL of Big Blue Button (use "bbb-conf --secret" to get it)',
62
                        max_length=400,
63
                        verbose_name='BBB URL',
64
                    ),
65
                ),
66
                (
67
                    'shared_secret',
68
                    models.CharField(
69
                        help_text='Shared ecret (use "bbb-conf --secret" to get it)', max_length=128
70
                    ),
71
                ),
72
                (
73
                    'users',
74
                    models.ManyToManyField(
75
                        blank=True,
76
                        related_name='_resource_users_+',
77
                        related_query_name='+',
78
                        to='base.ApiUser',
79
                    ),
80
                ),
81
            ],
82
            options={
83
                'verbose_name': 'Big Blue Button',
84
            },
85
        ),
86
        migrations.CreateModel(
87
            name='Meeting',
88
            fields=[
89
                (
90
                    'id',
91
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
92
                ),
93
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
94
                ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
95
                ('guid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID')),
96
                ('name', models.TextField(verbose_name='Name')),
97
                ('idempotent_id', models.TextField(unique=True, verbose_name='Idempotent ID')),
98
                ('running', models.BooleanField(default=False, verbose_name='Is running?')),
99
                ('last_time_running', models.DateTimeField(null=True, verbose_name='Last time running')),
100
                ('create_parameters', models.TextField(null=True, verbose_name='Create parameters')),
101
                (
102
                    'resource',
103
                    models.ForeignKey(
104
                        on_delete=django.db.models.deletion.CASCADE,
105
                        related_name='meetings',
106
                        to='bbb.Resource',
107
                        verbose_name='Resource',
108
                    ),
109
                ),
110
            ],
111
        ),
112
    ]
passerelle/apps/bbb/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import hashlib
18
import json
19
import time
20
import uuid
21

  
22
from django.core.cache import cache
23
from django.core.exceptions import PermissionDenied
24
from django.db import models, transaction
25
from django.http import HttpResponseRedirect, JsonResponse
26
from django.shortcuts import get_object_or_404
27
from django.urls import reverse
28
from django.utils.functional import cached_property
29
from django.utils.timezone import now
30
from django.utils.translation import gettext_lazy as _
31

  
32
from passerelle.base.models import BaseResource, HTTPResource
33
from passerelle.utils.api import endpoint
34
from passerelle.utils.jsonresponse import APIError
35

  
36
from . import utils
37

  
38

  
39
def complete_url(request, data):
40
    data = dict(data)
41
    for key in data:
42
        if key == 'url' or key.endswith('_url') and data[key]:
43
            data[key] = request.build_absolute_uri(data[key])
44
    return data
45

  
46

  
47
class Resource(BaseResource, HTTPResource):
48
    bbb_url = models.URLField(
49
        max_length=400,
50
        verbose_name=_('BBB URL'),
51
        help_text=_('Base URL of Big Blue Button (use "bbb-conf --secret" to get it)'),
52
    )
53
    shared_secret = models.CharField(
54
        max_length=128, help_text=_('Shared ecret (use "bbb-conf --secret" to get it)')
55
    )
56
    category = _('Business Process Connectors')
57

  
58
    class Meta:
59
        verbose_name = _('Big Blue Button')
60

  
61
    @cached_property
62
    def bbb(self):
63
        return utils.BBB(url=self.bbb_url, shared_secret=self.shared_secret, session=self.requests)
64

  
65
    def check_status(self):
66
        try:
67
            self.bbb.get_meetings()
68
        except utils.BBB.BBBError as e:
69
            raise APIError(e)
70

  
71
    @endpoint(
72
        methods=['post'],
73
        name='meeting',
74
        perm='can_access',
75
        description_post=_('Create a meeting'),
76
        post={
77
            'request_body': {
78
                'schema': {
79
                    'application/json': {
80
                        'type': 'object',
81
                        'properties': {
82
                            'name': {
83
                                'type': 'string',
84
                            },
85
                            'idempotent_id': {
86
                                'type': 'string',
87
                            },
88
                            'create_parameters': {
89
                                'type': 'object',
90
                                'properties': {
91
                                    'logoutURL': {'type': 'string'},
92
                                },
93
                                'additionalProperties': True,
94
                            },
95
                        },
96
                        'required': ['name', 'idempotent_id'],
97
                        'unflatten': True,
98
                    }
99
                }
100
            }
101
        },
102
        parameters={
103
            'update': {
104
                'description': _('Update existing meeting'),
105
                'example_value': '1',
106
            },
107
        },
108
    )
109
    @transaction.atomic
110
    def meetings_endpoint(self, request, post_data, update=0):
111
        create_parameters = (
112
            json.dumps(post_data['create_parameters']) if post_data.get('create_parameters') else None
113
        )
114
        if update == '1':
115
            meeting, dummy = self.meetings.update_or_create(
116
                idempotent_id=post_data['idempotent_id'],
117
                defaults={
118
                    'name': post_data['name'],
119
                    'create_parameters': create_parameters,
120
                },
121
            )
122
        else:
123
            meeting, created = self.meetings.get_or_create(
124
                idempotent_id=post_data['idempotent_id'],
125
                defaults={
126
                    'name': post_data['name'],
127
                    'create_parameters': create_parameters,
128
                },
129
            )
130
            if not created:
131
                if meeting.name != post_data['name'] or meeting.create_kwargs != post_data.get(
132
                    'create_parameters', {}
133
                ):
134
                    raise APIError('meeting already exists with different name of create_parameters')
135
        try:
136
            meeting.create()
137
        except utils.BBB.BBBError as e:
138
            raise APIError(e)
139
        return {'data': complete_url(request, meeting.to_json())}
140

  
141
    @endpoint(
142
        methods=['get'],
143
        name='meeting',
144
        perm='can_access',
145
        pattern=r'^(?P<guid>[0-9a-f]{32})/?$',
146
        example_pattern='{guid}/',
147
        description_post=_('Get a meeting'),
148
        parameters={
149
            'guid': {
150
                'description': _('Meeting guid'),
151
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
152
            },
153
        },
154
    )
155
    def meeting_endpoint(self, request, guid):
156
        meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
157
        return {'data': complete_url(request, meeting.to_json())}
158

  
159
    @endpoint(
160
        methods=['get'],
161
        name='meeting',
162
        pattern=r'^(?P<guid>[0-9a-f]{32})/is-running/?$',
163
        example_pattern='{guid}/is-running/',
164
        description_post=_('Report if meeting is running'),
165
        parameters={
166
            'guid': {
167
                'description': _('Meeting guid'),
168
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
169
            },
170
        },
171
    )
172
    @transaction.atomic
173
    def is_running(self, request, guid):
174
        meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
175
        if (now() - meeting.updated).total_seconds() > 5:
176
            meeting.update_is_running()
177
        response = JsonResponse({'err': 0, 'data': meeting.running})
178
        response['Access-Control-Allow-Origin'] = '*'
179
        return response
180

  
181
    @endpoint(
182
        methods=['get'],
183
        name='meeting',
184
        pattern=r'^(?P<guid>[0-9a-f]{32})/join/agent/(?P<key>[^/]*)/?$',
185
        example_pattern='{guid}/join/agent/',
186
        description_post=_('Get a meeting'),
187
        parameters={
188
            'guid': {
189
                'description': _('Meeting guid'),
190
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
191
            },
192
            'full_name': {
193
                'description': _('Agent full name'),
194
                'example_value': 'John Doe',
195
            },
196
            'key': {
197
                'description': _('Secret key'),
198
                'example_value': '1234',
199
            },
200
        },
201
    )
202
    def join_agent(self, request, guid, full_name, key):
203
        meeting = get_object_or_404(Meeting, guid=guid)
204
        if key != meeting.agent_key:
205
            raise PermissionDenied
206
        url = meeting.create().join_url(full_name, self.bbb.ROLE_MODERATOR)
207
        return HttpResponseRedirect(url)
208

  
209
    @endpoint(
210
        methods=['get'],
211
        name='meeting',
212
        pattern=r'^(?P<guid>[0-9a-f]{32})/join/user/(?P<key>[^/]*)/?$',
213
        example_pattern='{guid}/join/user/',
214
        description_post=_('Get a meeting'),
215
        parameters={
216
            'guid': {
217
                'description': _('Meeting guid'),
218
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
219
            },
220
            'full_name': {
221
                'description': _('User full name'),
222
                'example_value': 'John Doe',
223
            },
224
            'key': {
225
                'description': _('Secret key'),
226
                'example_value': '1234',
227
            },
228
        },
229
    )
230
    def join_user(self, request, guid, full_name, key):
231
        meeting = get_object_or_404(Meeting, guid=guid)
232
        if key != meeting.user_key:
233
            raise PermissionDenied
234
        url = meeting.create().join_url(full_name, self.bbb.ROLE_VIEWER)
235
        return HttpResponseRedirect(url)
236

  
237
    def _make_endpoint_url(self, endpoint, rest):
238
        return reverse(
239
            'generic-endpoint',
240
            kwargs={'connector': 'bbb', 'endpoint': endpoint, 'slug': self.slug, 'rest': rest},
241
        )
242

  
243

  
244
class Meeting(models.Model):
245
    created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True)
246
    updated = models.DateTimeField(verbose_name=_('Updated'), auto_now=True)
247
    resource = models.ForeignKey(
248
        Resource, verbose_name=_('Resource'), on_delete=models.CASCADE, related_name='meetings'
249
    )
250
    guid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
251
    name = models.TextField(verbose_name=_('Name'))
252
    idempotent_id = models.TextField(verbose_name=_('Idempotent ID'), unique=True)
253
    running = models.BooleanField(verbose_name=_('Is running?'), default=False)
254
    last_time_running = models.DateTimeField(verbose_name=_('Last time running'), null=True)
255
    create_parameters = models.TextField('Create parameters', null=True)
256

  
257
    def _make_key(self, _for):
258
        return hashlib.sha1((self.resource.shared_secret + _for + self.meeting_id).encode()).hexdigest()[:6]
259

  
260
    @property
261
    def user_key(self):
262
        return self._make_key('user')
263

  
264
    @property
265
    def join_user_url(self):
266
        return self.resource._make_endpoint_url(
267
            endpoint='meeting', rest=f'{self.guid.hex}/join/user/{self.user_key}/'
268
        )
269

  
270
    @property
271
    def agent_key(self):
272
        return self._make_key('agent')
273

  
274
    @property
275
    def join_agent_url(self):
276
        return self.resource._make_endpoint_url(
277
            endpoint='meeting', rest=f'{self.guid.hex}/join/agent/{self.agent_key}/'
278
        )
279

  
280
    @property
281
    def url(self):
282
        return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/')
283

  
284
    @property
285
    def is_running_url(self):
286
        return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/')
287

  
288
    def to_json(self):
289
        return {
290
            'guid': self.guid.hex,
291
            'created': self.created,
292
            'updated': self.updated,
293
            'name': self.name,
294
            'idempotent_id': self.idempotent_id,
295
            'running': self.running,
296
            'last_time_running': self.last_time_running,
297
            'url': self.url,
298
            'join_user_url': self.join_user_url,
299
            'join_agent_url': self.join_agent_url,
300
            'is_running_url': self.is_running_url,
301
            'bbb_meeting_info': self.meeting_info(),
302
        }
303

  
304
    @property
305
    def meeting_id(self):
306
        return self.guid.hex
307

  
308
    def update_is_running(self):
309
        try:
310
            running = self.resource.bbb.is_meeting_running(self.meeting_id)['running'] == 'true'
311
        except self.resource.bbb.BBBError:
312
            return
313
        if self.running != running:
314
            self.running = running
315
            if running:
316
                self.last_time_running = now()
317
                self.save(update_fields=['updated', 'last_time_running', 'running'])
318
            else:
319
                self.save(update_fields=['updated', 'running'])
320

  
321
    @property
322
    def create_kwargs(self):
323
        return json.loads(self.create_parameters) if self.create_parameters else {}
324

  
325
    def create(self):
326
        return self.resource.bbb.meetings.create(
327
            name=self.name, meeting_id=self.meeting_id, **self.create_kwargs
328
        )
329

  
330
    @property
331
    def cache_key(self):
332
        return f'bbb_{self.resource.slug}_{self.guid.hex}'
333

  
334
    def meeting_info(self):
335
        start = time.time()
336
        try:
337
            data, timestamp = cache.get(self.cache_key)
338
        except TypeError:
339
            data = None
340
        if data is None or (start - timestamp) > 30:
341
            try:
342
                data = self.resource.bbb.meetings.get(meeting_id=self.meeting_id).attributes
343
            except utils.BBB.BBBError:
344
                data = data or {}
345
            cache.set(self.cache_key, (data, start))
346
        return data
passerelle/apps/bbb/templates/bbb/resource_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
passerelle/apps/bbb/utils.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import hashlib
18
import re
19
import xml.etree.ElementTree as ET
20

  
21
import requests
22
from django.utils.http import urlencode
23

  
24

  
25
class BBB:
26
    CALL_CREATE = 'create'
27
    CALL_END = 'end'
28
    CALL_GET_MEETING_INFO = 'getMeetingInfo'
29
    CALL_GET_MEETINGS = 'getMeetings'
30
    CALL_IS_MEETING_RUNNING = 'isMeetingRunning'
31
    CALL_JOIN = 'join'
32

  
33
    PARAM_ATTENDEE_PW = 'attendeePW'
34
    PARAM_CREATE_TIME = 'createTime'
35
    PARAM_FULL_NAME = 'fullName'
36
    PARAM_MEETING_ID = 'meetingID'
37
    PARAM_MODERATOR_PW = 'moderatorPW'
38
    PARAM_NAME = 'name'
39
    PARAM_ROLE = 'role'
40

  
41
    TAG_RETURN_CODE = 'returncode'
42
    TAG_MESSAGE_KEY = 'messageKey'
43
    TAG_MESSAGE = 'message'
44

  
45
    RETURN_CODE_SUCCESS = 'SUCCESS'
46
    RETURN_CODE_FAILED = 'FAILED'
47
    RETURN_CODES = [RETURN_CODE_SUCCESS, RETURN_CODE_FAILED]
48

  
49
    ROLE_MODERATOR = 'MODERATOR'
50
    ROLE_VIEWER = 'VIEWER'
51
    ROLES = [ROLE_MODERATOR, ROLE_VIEWER]
52

  
53
    MESSAGE_KEY_ID_NOT_UNIQUE = 'idNotUnique'
54
    MESSAGE_KEY_NOT_FOUND = 'notFound'
55
    MESSAGE_KEY_CHECKSUM_ERROR = 'checksumError'
56
    MESSAGE_KEY_SENT_END_MEETING_REQUEST = 'sentEndMeetingRequest'
57

  
58
    class BBBError(Exception):
59
        pass
60

  
61
    class FailedError(BBBError):
62
        def __init__(self, message, message_key=None):
63
            self.message_key = message_key
64
            super().__init__(message)
65

  
66
        def __repr__(self):
67
            return f'<{self.__class__.__name__} "{self}" message_key={self.message_key}>'
68

  
69
    class NotFoundError(FailedError):
70
        pass
71

  
72
    class IdNotUniqueError(FailedError):
73
        pass
74

  
75
    class ChecksumError(FailedError):
76
        pass
77

  
78
    MESSAGE_KEY_TO_CLASS = {
79
        MESSAGE_KEY_ID_NOT_UNIQUE: IdNotUniqueError,
80
        MESSAGE_KEY_NOT_FOUND: NotFoundError,
81
        MESSAGE_KEY_CHECKSUM_ERROR: ChecksumError,
82
    }
83

  
84
    @staticmethod
85
    def camel_to_snake(string):
86
        string = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)
87
        string = re.sub('(.)([0-9]+)', r'\1_\2', string)
88
        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', string).lower()
89

  
90
    def __init__(self, *, url, shared_secret, session=None):
91
        if not url.endswith('/'):
92
            raise self.BBBError(f'expected url ending with a slash, got {url!r}')
93
        self.url = url
94
        self.shared_secret = shared_secret
95
        self.http_session = session or requests.Session()
96
        self.meetings = self.Meetings(bbb=self)
97

  
98
    def _build_query(self, name, parameters):
99
        """Method to create valid API query
100
        to call on BigBlueButton. Because each query
101
        should have a encrypted checksum based on request Data.
102
        """
103
        query = urlencode(parameters)
104
        prepared = f'{name}{query}{self.shared_secret}'
105
        checksum = hashlib.sha1(prepared.encode('utf-8')).hexdigest()
106
        sep = '&' if query else ''
107
        result = f'{query}{sep}checksum={checksum}'
108
        return result
109

  
110
    def _http_get(self, url):
111
        try:
112
            raw_response = self.http_session.get(url)
113
            raw_response.raise_for_status()
114
        except requests.RequestException as e:
115
            raise self.BBBError('transport error', e)
116
        return raw_response
117

  
118
    @classmethod
119
    def _parse_xml_doc(cls, root, data):
120
        for child in root:
121
            if len(child) == 0:
122
                data[child.tag.split('}')[-1]] = child.text
123
            elif max(len(sub) for sub in child) == 0:
124
                # subobject case
125
                data[child.tag] = cls._parse_xml_doc(child, {})
126
            else:
127
                # array case
128
                data[child.tag] = [cls._parse_xml_doc(sub, {}) for sub in child]
129
        return data
130

  
131
    @classmethod
132
    def _parse_response(cls, response):
133
        try:
134
            root = ET.fromstring(response.content)
135
            if root.tag != 'response':
136
                raise ValueError('root tag is not response')
137
            data = {}
138
            cls._parse_xml_doc(root, data)
139
            if cls.TAG_RETURN_CODE not in data:
140
                raise ValueError(f'expected a return code, got {data!r}')
141
            return_code = data.get(cls.TAG_RETURN_CODE)
142
            if return_code not in cls.RETURN_CODES:
143
                raise ValueError(f'expected a return code, got {data!r}')
144
            data.pop(cls.TAG_RETURN_CODE)
145
            return return_code, data
146
        except Exception as e:
147
            raise cls.BBBError('invalid response', e)
148

  
149
    def _make_url(self, name, parameters):
150
        query = self._build_query(name, parameters)
151
        return f'{self.url}api/{name}?{query}'
152

  
153
    def _api_call(self, name, parameters):
154
        url = self._make_url(name, parameters)
155
        raw_response = self._http_get(url)
156
        return_code, data = self._parse_response(raw_response)
157
        if return_code == self.RETURN_CODE_FAILED:
158
            exception_class = self.MESSAGE_KEY_TO_CLASS.get(data.get(self.TAG_MESSAGE_KEY), self.FailedError)
159
            raise exception_class(
160
                message=data.get(self.TAG_MESSAGE, 'NO_MESSAGE_KEY'),
161
                message_key=data.get(self.TAG_MESSAGE_KEY),
162
            )
163
        return data
164

  
165
    def create_meeting(self, name, meeting_id, attendee_pw=None, moderator_pw=None, **kwargs):
166
        parameters = {
167
            self.PARAM_NAME: name,
168
            self.PARAM_MEETING_ID: meeting_id,
169
            self.PARAM_ATTENDEE_PW: (
170
                attendee_pw
171
                or hashlib.sha1((self.shared_secret + meeting_id + 'attendee').encode()).hexdigest()
172
            ),
173
            self.PARAM_MODERATOR_PW: (
174
                moderator_pw
175
                or hashlib.sha1((self.shared_secret + meeting_id + 'moderator').encode()).hexdigest()
176
            ),
177
        }
178
        return self._api_call(self.CALL_CREATE, dict(parameters, **kwargs))
179

  
180
    def get_meeting_info(self, meeting_id):
181
        parameters = {
182
            self.PARAM_MEETING_ID: meeting_id,
183
        }
184
        return self._api_call(self.CALL_GET_MEETING_INFO, parameters)
185

  
186
    def is_meeting_running(self, meeting_id):
187
        parameters = {
188
            self.PARAM_MEETING_ID: meeting_id,
189
        }
190
        try:
191
            return self._api_call(self.CALL_IS_MEETING_RUNNING, parameters)
192
        except self.NotFoundError:
193
            return False
194

  
195
    def end_meeting(self, meeting_id):
196
        parameters = {
197
            self.PARAM_MEETING_ID: meeting_id,
198
        }
199
        try:
200
            response = self._api_call(self.CALL_END, parameters)
201
        except self.NotFoundError:
202
            return False
203
        if response[self.TAG_MESSAGE_KEY] == self.MESSAGE_KEY_SENT_END_MEETING_REQUEST:
204
            return True
205
        raise self.BBBError(f'expected notFound or sentEndMeetingRequest, got {response!r}')
206

  
207
    def get_meetings(self):
208
        return self._api_call(self.CALL_GET_MEETINGS, {})
209

  
210
    def make_join_url(self, full_name, meeting_id, role, create_time=None, **kwargs):
211
        if role not in self.ROLES:
212
            raise self.BBBError(f'expected a role value, got {role!r}')
213
        parameters = {
214
            self.PARAM_FULL_NAME: full_name,
215
            self.PARAM_MEETING_ID: meeting_id,
216
            self.PARAM_ROLE: role,
217
        }
218
        if create_time is not None:
219
            parameters[self.PARAM_CREATE_TIME] = create_time
220
        return self._make_url(self.CALL_JOIN, parameters)
221

  
222
    class Meeting:
223
        is_running = False
224
        meeting_name = None
225
        meeting_id = None
226

  
227
        def __init__(self, bbb: 'BBB', **kwargs):
228
            self.bbb = bbb
229
            # came to snake case
230
            parameters = {BBB.camel_to_snake(key): value for key, value in kwargs.items()}
231
            self.message = {k: v for k, v in parameters.items() if k.startswith('message')}
232
            self.meeting_id = parameters.pop('meeting_id')
233
            self.meeting_name = parameters.pop('meeting_name')
234
            self.attributes = {k: v for k, v in parameters.items() if not k.startswith('message')}
235

  
236
        def update(self):
237
            data = self.bbb.get_meeting_info(self.meeting_id)
238
            parameters = {
239
                BBB.camel_to_snake(key): value
240
                for key, value in data.items()
241
                if key not in ['meetingID', 'meetingName']
242
            }
243
            self.message = {k: v for k, v in parameters.items() if k.startswith('message')}
244
            self.attributes = {k: v for k, v in parameters.items() if not k.startswith('message')}
245

  
246
        def update_is_running(self):
247
            self.is_running = self.bbb.is_meeting_running(self.meeting_id)['running'] == 'true'
248
            return self.is_running
249

  
250
        def end(self):
251
            try:
252
                return self.bbb.end_meeting(self.meeting_id)
253
            finally:
254
                self.is_running = False
255

  
256
        def join_url(self, full_name, role):
257
            return self.bbb.make_join_url(
258
                full_name=full_name,
259
                role=role,
260
                meeting_id=self.meeting_id,
261
                create_time=self.attributes['create_time'],
262
            )
263

  
264
        def to_dict(self):
265
            return {k: v for k, v in self.__dict__.items() if k not in ['bbb']}
266

  
267
        def __repr__(self):
268
            return f'<Meeting name={self.meeting_name} id={self.meeting_id}>'
269

  
270
    class Meetings:
271
        def __init__(self, bbb: 'BBB'):
272
            self.bbb = bbb
273

  
274
        def create(self, name, meeting_id, **kwargs):
275
            return BBB.Meeting(
276
                self.bbb,
277
                meeting_name=name,
278
                **self.bbb.create_meeting(name=name, meeting_id=meeting_id, **kwargs),
279
            )
280

  
281
        def get(self, meeting_id):
282
            return BBB.Meeting(self.bbb, **self.bbb.get_meeting_info(meeting_id=meeting_id))
283

  
284
        def all(self):
285
            response = self.bbb.get_meetings()
286
            for response_meeting in response.get('meetings', []):
287
                yield BBB.Meeting(self.bbb, **response_meeting)
passerelle/settings.py
132 132
    'passerelle.apps.atal',
133 133
    'passerelle.apps.atos_genesys',
134 134
    'passerelle.apps.base_adresse',
135
    'passerelle.apps.bbb',
135 136
    'passerelle.apps.bdp',
136 137
    'passerelle.apps.cartads_cs',
137 138
    'passerelle.apps.choosit',
tests/test_bbb.py
1
# Copyright (C) 2021  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16
import datetime
17
import json
18
import uuid
19

  
20
import mock
21
import pytest
22

  
23
import passerelle.apps.bbb.utils as bbb_utils
24

  
25
from . import utils
26

  
27
BBB_URL = 'https://example.com/bigbluebutton/'
28
SHARED_SECRET = 'ABCD'
29
SLUG = 'test'
30
MEETING_NAME = 'RDV'
31
MEETING_ID = 'a' * 32
32
UUID = uuid.UUID(MEETING_ID)
33
IDEMPOTENT_ID = '10-1999'
34
LOGOUT_URL = 'https://portal/'
35

  
36

  
37
@pytest.fixture
38
def connector(db):
39
    from passerelle.apps.bbb.models import Resource
40

  
41
    return utils.setup_access_rights(
42
        Resource.objects.create(
43
            slug=SLUG,
44
            bbb_url=BBB_URL,
45
            shared_secret=SHARED_SECRET,
46
        )
47
    )
48

  
49

  
50
class TestManage:
51
    pytestmark = pytest.mark.django_db
52

  
53
    @pytest.fixture
54
    def app(self, app, admin_user):
55
        from .test_manager import login
56

  
57
        login(app)
58
        return app
59

  
60
    def test_homepage(self, app, connector):
61
        app.get(f'/bbb/{SLUG}/')
62

  
63

  
64
class TestBBB:
65
    @pytest.fixture
66
    def bbb(self):
67
        return bbb_utils.BBB(url=BBB_URL, shared_secret=SHARED_SECRET)
68

  
69
    @pytest.fixture
70
    def mock(self):
71
        with mock.patch('requests.Session.send') as requests_send:
72
            requests_send.return_value = mock.Mock()
73
            yield requests_send
74

  
75
    def test_create_failure(self, bbb, mock):
76
        mock.return_value.status_code = 200
77
        mock.return_value.content = '<response/>'
78

  
79
        with pytest.raises(bbb.BBBError):
80
            bbb.create_meeting(name=MEETING_NAME, meeting_id=MEETING_ID)
81

  
82
    class TestCreate:
83
        @pytest.fixture
84
        def mock(self, mock):
85
            mock.return_value.content = '''<response>
86
    <returncode>SUCCESS</returncode>
87
    <message>ok</message>
88
    <meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID>
89
</response>'''
90
            return mock
91

  
92
        def test_create(self, bbb, mock):
93
            result = bbb.create_meeting(name=MEETING_NAME, meeting_id=MEETING_ID)
94
            prepared_request = mock.call_args[0][0]
95
            assert prepared_request.url == (
96
                'https://example.com/bigbluebutton/api/create?'
97
                'name=RDV&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&attendeePW=6256459f2baead794d599a1ad21a07fd4bc5743a'
98
                '&moderatorPW=2ebb4732fcbdd5a9f0751c3ffa7a5f51f755f980&checksum=4ac7d65fe6beb6f86fa5a13d6d8e0d1fadee0b50'
99
            )
100
            assert result == {'meetingID': MEETING_ID, 'message': 'ok'}
101

  
102
        def test_meetings_create(self, bbb, mock):
103
            meeting = bbb.meetings.create(name=MEETING_NAME, meeting_id=MEETING_ID)
104
            prepared_request = mock.call_args[0][0]
105
            assert prepared_request.url == (
106
                'https://example.com/bigbluebutton/api/create?'
107
                'name=RDV&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&attendeePW=6256459f2baead794d599a1ad21a07fd4bc5743a'
108
                '&moderatorPW=2ebb4732fcbdd5a9f0751c3ffa7a5f51f755f980&checksum=4ac7d65fe6beb6f86fa5a13d6d8e0d1fadee0b50'
109
            )
110
            assert meeting.meeting_name == MEETING_NAME
111
            assert meeting.meeting_id == MEETING_ID
112
            assert meeting.attributes == {}
113
            assert meeting.message == {'message': 'ok'}
114

  
115
    class TestGetMeetingInfo:
116
        @pytest.fixture
117
        def mock(self, mock):
118
            mock.return_value.content = '''<response>
119
    <returncode>SUCCESS</returncode>
120
    <meetingName>RDV</meetingName>
121
    <message>ok</message>
122
    <meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID>
123
    <metadata>
124
      <coin>1</coin>
125
    </metadata>
126
    <others>
127
      <other>
128
        <a>1</a>
129
      </other>
130
    </others>
131
</response>'''
132
            return mock
133

  
134
        def test_get_meeting_info(self, bbb, mock):
135
            result = bbb.get_meeting_info(meeting_id=MEETING_ID)
136
            prepared_request = mock.call_args[0][0]
137
            assert (
138
                prepared_request.url == 'https://example.com/bigbluebutton/api/getMeetingInfo'
139
                '?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=4d71906c30e6e62d8b9f1a3e57233bb64126e0da'
140
            )
141
            assert result == {
142
                'meetingName': MEETING_NAME,
143
                'meetingID': MEETING_ID,
144
                'message': 'ok',
145
                'metadata': {'coin': '1'},
146
                'others': [{'a': '1'}],
147
            }
148

  
149
        def test_meetings_get(self, bbb, mock):
150
            meeting = bbb.meetings.get(meeting_id=MEETING_ID)
151
            prepared_request = mock.call_args[0][0]
152
            assert (
153
                prepared_request.url == 'https://example.com/bigbluebutton/api/getMeetingInfo'
154
                '?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=4d71906c30e6e62d8b9f1a3e57233bb64126e0da'
155
            )
156
            assert meeting.meeting_name == MEETING_NAME
157
            assert meeting.meeting_id == MEETING_ID
158
            assert meeting.attributes == {
159
                'metadata': {'coin': '1'},
160
                'others': [{'a': '1'}],
161
            }
162
            assert meeting.message == {'message': 'ok'}
163

  
164
    class TestMakeJoinURL:
165
        def test_make_join_url(self, bbb):
166
            assert bbb.make_join_url(
167
                meeting_id=MEETING_ID, full_name='John Doe', role=bbb.ROLE_MODERATOR
168
            ) == (
169
                'https://example.com/bigbluebutton/api/join?fullName=John+Doe'
170
                '&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=MODERATOR'
171
                '&checksum=838762e70a6fe3257ea81f7b27acbde9945acaf9'
172
            )
173

  
174
        def test_meeting_join_url(self, bbb):
175
            meeting = bbb.Meeting(bbb, meeting_name=None, meeting_id=MEETING_ID, create_time=1234)
176
            assert meeting.join_url(full_name='John Doe', role=bbb.ROLE_MODERATOR) == (
177
                'https://example.com/bigbluebutton/api/join?fullName=John+Doe'
178
                '&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=MODERATOR'
179
                '&createTime=1234&checksum=07a7e7fd233dd213bd6dfc5332cc842afac56ba5'
180
            )
181

  
182
    class TestEndMeeting:
183
        @pytest.fixture
184
        def mock(self, mock):
185
            mock.return_value.content = '''<response>
186
    <returncode>SUCCESS</returncode>
187
    <messageKey>sentEndMeetingRequest</messageKey>
188
</response>'''
189
            return mock
190

  
191
        def test_end_meeting(self, bbb, mock):
192
            result = bbb.end_meeting(meeting_id=MEETING_ID)
193
            prepared_request = mock.call_args[0][0]
194
            assert (
195
                prepared_request.url == 'https://example.com/bigbluebutton/api/end'
196
                '?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698'
197
            )
198
            assert result is True
199

  
200
        def test_meeting_end(self, bbb, mock):
201
            meeting = bbb.Meeting(bbb, meeting_name=None, meeting_id=MEETING_ID)
202
            result = meeting.end()
203
            prepared_request = mock.call_args[0][0]
204
            assert (
205
                prepared_request.url == 'https://example.com/bigbluebutton/api/end'
206
                '?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698'
207
            )
208
            assert result is True
209

  
210
    class TestEndMeetingIfNotFound:
211
        @pytest.fixture
212
        def mock(self, mock):
213
            mock.return_value.content = '''<response>
214
    <returncode>FAILED</returncode>
215
    <messageKey>notFound</messageKey>
216
</response>'''
217
            return mock
218

  
219
        def test_end_meeting(self, bbb, mock):
220
            result = bbb.end_meeting(meeting_id=MEETING_ID)
221
            prepared_request = mock.call_args[0][0]
222
            assert (
223
                prepared_request.url == 'https://example.com/bigbluebutton/api/end'
224
                '?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698'
225
            )
226
            assert result is False
227

  
228
    class TestGetMeetings:
229
        @pytest.fixture
230
        def mock(self, mock):
231
            mock.return_value.content = '''<response>
232
    <returncode>SUCCESS</returncode>
233
    <meetings>
234
      <meeting>
235
        <meetingName>RDV</meetingName>
236
        <message>ok</message>
237
        <meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID>
238
        <metadata>
239
          <coin>1</coin>
240
        </metadata>
241
        <others>
242
          <other>
243
            <a>1</a>
244
          </other>
245
        </others>
246
      </meeting>
247
    </meetings>
248
</response>'''
249
            return mock
250

  
251
        def test_get_meetings(self, bbb, mock):
252
            result = bbb.get_meetings()
253
            prepared_request = mock.call_args[0][0]
254
            assert (
255
                prepared_request.url
256
                == 'https://example.com/bigbluebutton/api/getMeetings?checksum=64bf453f633be1f9d2c7b0f6bed4a4702dbcc41e'
257
            )
258
            assert result == {
259
                'meetings': [
260
                    {
261
                        'meetingID': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
262
                        'meetingName': 'RDV',
263
                        'message': 'ok',
264
                        'metadata': {'coin': '1'},
265
                        'others': [{'a': '1'}],
266
                    }
267
                ]
268
            }
269

  
270
        def test_meetings_all(self, bbb, mock):
271
            result = list(bbb.meetings.all())
272
            prepared_request = mock.call_args[0][0]
273
            assert (
274
                prepared_request.url
275
                == 'https://example.com/bigbluebutton/api/getMeetings?checksum=64bf453f633be1f9d2c7b0f6bed4a4702dbcc41e'
276
            )
277
            assert len(result) == 1
278
            meeting = result[0]
279
            assert meeting.meeting_name == MEETING_NAME
280
            assert meeting.meeting_id == MEETING_ID
281
            assert meeting.attributes == {
282
                'metadata': {'coin': '1'},
283
                'others': [{'a': '1'}],
284
            }
285
            assert meeting.message == {'message': 'ok'}
286

  
287

  
288
class TestAPI:
289
    @pytest.fixture(autouse=True)
290
    def stable(self, freezer):
291
        from passerelle.apps.bbb.models import Meeting
292

  
293
        freezer.move_to('2022-01-01T12:00:00Z')
294
        with mock.patch.object(Meeting._meta.get_field('guid'), 'default', UUID):
295
            yield None
296

  
297
    @pytest.fixture
298
    def meetings_create(self):
299
        with mock.patch('passerelle.apps.bbb.utils.BBB.Meetings.create') as create:
300
            yield create
301

  
302
    @pytest.fixture
303
    def meetings_get(self):
304
        with mock.patch('passerelle.apps.bbb.utils.BBB.Meetings.get') as get:
305
            yield get
306

  
307
    @pytest.fixture
308
    def is_meeting_running(self):
309
        with mock.patch('passerelle.apps.bbb.utils.BBB.is_meeting_running') as is_running:
310
            yield is_running
311

  
312
    class TestCreate:
313
        def test_normal(self, app, connector, meetings_create, meetings_get):
314
            meeting = bbb_utils.BBB.Meeting(
315
                None, meeting_id=MEETING_ID, meeting_name=MEETING_NAME, logout_url=LOGOUT_URL
316
            )
317
            meetings_create.return_value = meeting
318
            meetings_get.return_value = meeting
319
            response = app.post_json(
320
                f'/bbb/{SLUG}/meeting',
321
                params={
322
                    'name': MEETING_NAME,
323
                    'idempotent_id': IDEMPOTENT_ID,
324
                    'create_parameters/logoutUrl': LOGOUT_URL,
325
                },
326
            )
327
            assert meetings_create.call_args == mock.call(
328
                logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME
329
            )
330
            assert response.json['err'] == 0
331
            assert response.json['data'] == {
332
                'created': '2022-01-01T12:00:00Z',
333
                'updated': '2022-01-01T12:00:00Z',
334
                'guid': MEETING_ID,
335
                'idempotent_id': IDEMPOTENT_ID,
336
                'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
337
                'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
338
                'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/',
339
                'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/',
340
                'last_time_running': None,
341
                'name': 'RDV',
342
                'running': False,
343
                'bbb_meeting_info': {
344
                    'logout_url': LOGOUT_URL,
345
                },
346
            }
347
            meeting = connector.meetings.get()
348
            assert meeting.meeting_id == MEETING_ID
349
            assert meeting.name == MEETING_NAME
350
            assert meeting.create_kwargs == {'logoutUrl': LOGOUT_URL}
351

  
352
        def test_error(self, app, connector, meetings_create):
353
            meetings_create.side_effect = bbb_utils.BBB.BBBError('coin')
354
            response = app.post_json(
355
                f'/bbb/{SLUG}/meeting',
356
                params={
357
                    'name': MEETING_NAME,
358
                    'idempotent_id': IDEMPOTENT_ID,
359
                    'create_parameters': {
360
                        'logoutUrl': LOGOUT_URL,
361
                    },
362
                },
363
            )
364
            assert response.json['err'] == 1
365
            assert response.json['err_desc'] == 'coin'
366

  
367
        def test_update(self, app, connector, meetings_create, meetings_get):
368
            connector.meetings.create(
369
                idempotent_id=IDEMPOTENT_ID,
370
                name=MEETING_NAME,
371
                guid=UUID,
372
            )
373
            response = app.post_json(
374
                f'/bbb/{SLUG}/meeting',
375
                params={
376
                    'name': MEETING_NAME,
377
                    'idempotent_id': IDEMPOTENT_ID,
378
                    'create_parameters/logoutUrl': LOGOUT_URL,
379
                },
380
            )
381
            assert response.json['err'] == 1
382
            assert (
383
                response.json['err_desc'] == 'meeting already exists with different name of create_parameters'
384
            )
385

  
386
            meeting = bbb_utils.BBB.Meeting(
387
                None, meeting_id=MEETING_ID, meeting_name=MEETING_NAME, logout_url=LOGOUT_URL
388
            )
389
            meetings_create.return_value = meeting
390
            meetings_get.return_value = meeting
391
            response = app.post_json(
392
                f'/bbb/{SLUG}/meeting?update=1',
393
                params={
394
                    'name': MEETING_NAME,
395
                    'idempotent_id': IDEMPOTENT_ID,
396
                    'create_parameters/logoutUrl': LOGOUT_URL,
397
                },
398
            )
399
            assert response.json['err'] == 0
400

  
401
    class TestGet:
402
        def test_ok(self, app, connector, meetings_get, freezer):
403
            connector.meetings.create(
404
                guid=UUID,
405
                idempotent_id=IDEMPOTENT_ID,
406
                name=MEETING_NAME,
407
                create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}),
408
            )
409
            meeting = bbb_utils.BBB.Meeting(
410
                bbb=connector.bbb,
411
                meeting_id=MEETING_ID,
412
                meeting_name=MEETING_NAME,
413
                logout_url=LOGOUT_URL,
414
                create_time=1234,
415
            )
416
            meetings_get.return_value = meeting
417
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/')
418
            assert response.json['err'] == 0
419
            assert response.json['data'] == {
420
                'created': '2022-01-01T12:00:00Z',
421
                'updated': '2022-01-01T12:00:00Z',
422
                'guid': MEETING_ID,
423
                'idempotent_id': IDEMPOTENT_ID,
424
                'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
425
                'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
426
                'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/',
427
                'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/',
428
                'last_time_running': None,
429
                'name': 'RDV',
430
                'running': False,
431
                'bbb_meeting_info': {
432
                    'create_time': 1234,
433
                    'logout_url': LOGOUT_URL,
434
                },
435
            }
436
            # test cache
437
            freezer.move_to(datetime.timedelta(seconds=100))
438
            meetings_get.side_effect = bbb_utils.BBB.BBBError('boom!')
439
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/')
440
            assert response.json['err'] == 0
441
            assert response.json['data'] == {
442
                'created': '2022-01-01T12:00:00Z',
443
                'updated': '2022-01-01T12:00:00Z',
444
                'guid': MEETING_ID,
445
                'idempotent_id': IDEMPOTENT_ID,
446
                'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
447
                'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/',
448
                'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/',
449
                'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/',
450
                'last_time_running': None,
451
                'name': 'RDV',
452
                'running': False,
453
                'bbb_meeting_info': {
454
                    'create_time': 1234,
455
                    'logout_url': LOGOUT_URL,
456
                },
457
            }
458

  
459
    class TestIsRunning:
460
        def test_ok(self, app, connector, meetings_get, is_meeting_running, freezer):
461
            connector.meetings.create(
462
                guid=UUID,
463
                idempotent_id=IDEMPOTENT_ID,
464
                name=MEETING_NAME,
465
                create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}),
466
            )
467
            meeting = bbb_utils.BBB.Meeting(
468
                bbb=connector.bbb,
469
                meeting_id=MEETING_ID,
470
                meeting_name=MEETING_NAME,
471
                logout_url=LOGOUT_URL,
472
                create_time=1234,
473
            )
474
            freezer.move_to(datetime.timedelta(seconds=6))
475
            meetings_get.return_value = meeting
476
            is_meeting_running.return_value = {'running': 'true'}
477
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/')
478
            assert response['Access-Control-Allow-Origin'] == '*'
479
            assert response.json['err'] == 0
480
            assert response.json['data'] is True
481
            assert connector.meetings.get().running is True
482
            # test cache
483
            is_meeting_running.return_value = {'running': 'false'}
484
            is_meeting_running.side_effect = bbb_utils.BBB.BBBError('boom!')
485
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/')
486
            assert response.json['err'] == 0
487
            assert response.json['data'] is True
488
            # test cache expired
489
            freezer.move_to(datetime.timedelta(seconds=6))
490
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/')
491
            assert response.json['err'] == 0
492
            assert response.json['data'] is True
493
            is_meeting_running.side_effect = None
494
            response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/')
495
            assert response.json['err'] == 0
496
            assert response.json['data'] is False
497

  
498
    class TestJoin:
499
        def test_join_user(self, app, connector, meetings_create):
500
            model_meeting = connector.meetings.create(
501
                guid=UUID, name=MEETING_NAME, create_parameters=json.dumps({'logoutUrl': LOGOUT_URL})
502
            )
503
            meeting = bbb_utils.BBB.Meeting(
504
                bbb=connector.bbb,
505
                meeting_id=MEETING_ID,
506
                meeting_name=MEETING_NAME,
507
                logout_url=LOGOUT_URL,
508
                create_time=1234,
509
            )
510
            meetings_create.return_value = meeting
511

  
512
            response = app.get(
513
                f'/bbb/{SLUG}/meeting/{MEETING_ID}/join/user/{model_meeting.user_key}/',
514
                params={'full_name': 'John Doe'},
515
            )
516
            assert meetings_create.call_args == mock.call(
517
                logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME
518
            )
519
            assert response.location == (
520
                'https://example.com/bigbluebutton/api/join'
521
                '?fullName=John+Doe&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
522
                '&role=VIEWER&createTime=1234&checksum=1851e34e269a5063605bc64589ea53ded2f9744d'
523
            )
524

  
525
        def test_join_agent(self, app, connector, meetings_create):
526
            model_meeting = connector.meetings.create(
527
                guid=UUID, name=MEETING_NAME, create_parameters=json.dumps({'logoutUrl': LOGOUT_URL})
528
            )
529
            meeting = bbb_utils.BBB.Meeting(
530
                bbb=connector.bbb,
531
                meeting_id=MEETING_ID,
532
                meeting_name=MEETING_NAME,
533
                logout_url=LOGOUT_URL,
534
                create_time=1234,
535
            )
536
            meetings_create.return_value = meeting
537

  
538
            response = app.get(
539
                f'/bbb/{SLUG}/meeting/{MEETING_ID}/join/agent/{model_meeting.agent_key}/',
540
                params={'full_name': 'John Doe'},
541
            )
542
            assert meetings_create.call_args == mock.call(
543
                logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME
544
            )
545
            assert response.location == (
546
                'https://example.com/bigbluebutton/api/join'
547
                '?fullName=John+Doe&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
548
                '&role=MODERATOR&createTime=1234&checksum=07a7e7fd233dd213bd6dfc5332cc842afac56ba5'
549
            )
0
-