from unittest import mock import orjson import requests import responses from django.core.signing import Signer from django.http import HttpResponseRedirect from django.test import override_settings from typing_extensions import override from zerver.lib.test_classes import ZulipTestCase from zerver.lib.url_encoding import append_url_query_string @override_settings(VIDEO_ZOOM_SERVER_TO_SERVER_ACCOUNT_ID=None) class ZoomVideoCallTestUserAuth(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") self.login_user(self.user) def test_register_zoom_request_no_settings(self) -> None: with self.settings(VIDEO_ZOOM_CLIENT_ID=None): response = self.client_get("/calls/zoom/register") self.assert_json_error( response, "Zoom credentials have not been configured", ) def test_register_zoom_request(self) -> None: response = self.client_get("/calls/zoom/register") self.assertEqual(response.status_code, 302) @responses.activate def test_create_zoom_video_and_audio_links(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "oldtoken", "expires_in": -60}, ) response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assertEqual(response.status_code, 200) # Test creating a video link responses.replace( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "newtoken", "expires_in": 60}, ) responses.add( responses.POST, "https://api.zoom.us/v2/users/me/meetings", json={"join_url": "example.com"}, ) response = self.client_post("/json/calls/zoom/create", {"is_video_call": "true"}) self.assertEqual( responses.calls[-1].request.url, "https://api.zoom.us/v2/users/me/meetings", ) assert responses.calls[-1].request.body is not None self.assertEqual( orjson.loads(responses.calls[-1].request.body), { "settings": { "host_video": True, "participant_video": True, }, "default_password": True, }, ) self.assertEqual( responses.calls[-1].request.headers["Authorization"], "Bearer newtoken", ) json = self.assert_json_success(response) self.assertEqual(json["url"], "example.com") # Test creating an audio link responses.replace( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "newtoken", "expires_in": 60}, ) responses.add( responses.POST, "https://api.zoom.us/v2/users/me/meetings", json={"join_url": "example.com"}, ) response = self.client_post("/json/calls/zoom/create", {"is_video_call": "false"}) self.assertEqual( responses.calls[-1].request.url, "https://api.zoom.us/v2/users/me/meetings", ) assert responses.calls[-1].request.body is not None self.assertEqual( orjson.loads(responses.calls[-1].request.body), { "settings": { "host_video": False, "participant_video": False, }, "default_password": True, }, ) self.assertEqual( responses.calls[-1].request.headers["Authorization"], "Bearer newtoken", ) json = self.assert_json_success(response) self.assertEqual(json["url"], "example.com") # Test for authentication error self.logout() self.login_user(self.user) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Invalid Zoom access token") def test_create_zoom_realm_redirect(self) -> None: response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zephyr","sid":"somesid"}'}, ) self.assertEqual(response.status_code, 302) self.assertIn("http://zephyr.testserver/", response["Location"]) self.assertIn("somesid", response["Location"]) def test_create_zoom_sid_error(self) -> None: response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zulip","sid":"bad"}'}, ) self.assert_json_error(response, "Invalid Zoom session identifier") @responses.activate def test_create_zoom_credential_error(self) -> None: responses.add(responses.POST, "https://zoom.example.com/oauth/token", status=400) response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assert_json_error(response, "Invalid Zoom credentials") @responses.activate def test_create_zoom_refresh_error(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token", "expires_in": -60}, ) response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assertEqual(response.status_code, 200) responses.replace(responses.POST, "https://zoom.example.com/oauth/token", status=400) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Invalid Zoom access token") @responses.activate def test_create_zoom_request_error(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token"}, ) responses.add( responses.POST, "https://api.zoom.us/v2/users/me/meetings", status=400, ) response = self.client_get( "/calls/zoom/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assertEqual(response.status_code, 200) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Failed to create Zoom call") responses.replace( responses.POST, "https://api.zoom.us/v2/users/me/meetings", status=401, ) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Invalid Zoom access token") @responses.activate def test_deauthorize_zoom_user(self) -> None: response = self.client_post( "/calls/zoom/deauthorize", """\ { "event": "app_deauthorized", "payload": { "user_data_retention": "false", "account_id": "EabCDEFghiLHMA", "user_id": "z9jkdsfsdfjhdkfjQ", "signature": "827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000", "deauthorization_time": "2019-06-17T13:52:28.632Z", "client_id": "ADZ9k9bTWmGUoUbECUKU_a" } } """, content_type="application/json", ) self.assert_json_success(response) class WebexVideoCallTestOAuth(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") self.login_user(self.user) def test_register_webex_request_no_settings(self) -> None: with self.settings(VIDEO_WEBEX_CLIENT_ID=None): response = self.client_get("/calls/webex/register") self.assert_json_error( response, "Webex credentials have not been configured", ) def test_register_webex_request(self) -> None: response = self.client_get("/calls/webex/register") self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith("https://webexapis.com/v1/authorize")) @responses.activate def test_create_webex_access_token(self) -> None: responses.add( responses.POST, "https://webexapis.com/v1/access_token", json={"access_token": "oldtoken", "expires_in": -60}, ) response = self.client_get( "/calls/webex/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assertEqual(response.status_code, 200) @responses.activate def test_create_webex_adhoc_meeting(self) -> None: responses.add( responses.POST, "https://webexapis.com/v1/access_token", json={"access_token": "token", "expires_in": 3600}, ) response = self.client_get( "/calls/webex/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) self.assertEqual(response.status_code, 200) responses.add( responses.POST, "https://webexapis.com/v1/rooms", json={"id": "room_id_123"}, status=200, ) responses.add( responses.POST, "https://webexapis.com/v1/meetings", json={ "id": "meeting_id_456", "webLink": "https://webex.com/test", }, status=200, ) response = self.client_post("/json/calls/webex/create") result = self.assert_json_success(response) self.assertEqual(result["url"], "https://webex.com/test") # Test for token removal on logout. self.logout() self.login_user(self.user) response = self.client_post("/json/calls/webex/create") self.assert_json_error(response, "Invalid Webex access token") @responses.activate def test_create_webex_personal_room_meeting(self) -> None: responses.add( responses.POST, "https://webexapis.com/v1/access_token", json={"access_token": "token", "expires_in": 3600}, ) self.client_get( "/calls/webex/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) responses.add( responses.POST, "https://webexapis.com/v1/rooms", status=403, ) responses.add( responses.POST, "https://webexapis.com/v1/meetings", json={ "id": "meeting_id_456", "webLink": "https://webex.com/test", }, status=200, ) result = self.client_post("/json/calls/webex/create") assert responses.calls[-1].request.body is not None payload = orjson.loads(responses.calls[-1].request.body) self.assertEqual(payload["scheduledType"], "personalRoomMeeting") json = self.assert_json_success(result) self.assertEqual(json["url"], "https://webex.com/test") def test_create_webex_realm_redirect(self) -> None: response = self.client_get( "/calls/webex/complete", {"code": "code", "state": '{"realm":"lear","sid":"somesid"}'}, ) self.assertEqual(response.status_code, 302) self.assertIn("http://lear.testserver/", response["Location"]) self.assertIn("somesid", response["Location"]) @responses.activate def test_webex_public_room_creation_failure(self) -> None: responses.add( responses.POST, "https://webexapis.com/v1/access_token", json={"access_token": "token", "expires_in": 3600}, ) self.client_get( "/calls/webex/complete", {"code": "code", "state": '{"realm":"zulip","sid":""}'}, ) responses.add( responses.POST, "https://webexapis.com/v1/rooms", status=500, ) response = self.client_post("/json/calls/webex/create") self.assert_json_error(response, "Failed to create Webex public room.") class ZoomVideoCallTestServerAuth(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") self.login_user(self.user) self.user_zoom_meeting_url = ( f"https://api.zoom.us/v2/users/{self.user.delivery_email}/meetings" ) @responses.activate def test_zoom_invalid_settings(self) -> None: with self.settings(VIDEO_ZOOM_CLIENT_ID=None): response = self.client_post("/json/calls/zoom/create") self.assert_json_error( response, "Zoom credentials have not been configured", ) responses.add(responses.POST, "https://zoom.example.com/oauth/token", status=400) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Invalid Zoom credentials") @responses.activate def test_zoom_invalid_access_token_error(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token"}, ) responses.add( responses.POST, self.user_zoom_meeting_url, status=400, json={"code": 124, "message": "API key expired"}, ) with self.assertLogs(level="ERROR") as error_log: response = self.client_post("/json/calls/zoom/create") self.assertEqual( error_log.output[0], "ERROR:root:Unexpected Zoom error 124: API key expired", ) self.assert_json_error(response, "Failed to create Zoom call") @responses.activate def test_zoom_unknown_email_error(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token"}, ) responses.add(responses.POST, self.user_zoom_meeting_url, status=400, json={"code": 1001}) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Unknown Zoom user email") @responses.activate def test_zoom_error_api_response_code_unknown(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token"}, ) responses.add(responses.POST, self.user_zoom_meeting_url, status=400, json={"code": 300}) response = self.client_post("/json/calls/zoom/create") self.assert_json_error(response, "Failed to create Zoom call") @responses.activate def test_zoom_create_video_call(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token", "expires_in": 3599}, ) responses.add( responses.POST, self.user_zoom_meeting_url, json={"join_url": "example.com"}, ) response = self.client_post("/json/calls/zoom/create", {"is_video_call": "true"}) self.assertEqual( responses.calls[-1].request.url, self.user_zoom_meeting_url, ) assert responses.calls[-1].request.body is not None self.assertEqual( orjson.loads(responses.calls[-1].request.body), { "settings": { "host_video": True, "participant_video": True, }, "default_password": True, }, ) self.assertEqual( responses.calls[-1].request.headers["Authorization"], "Bearer token", ) json = self.assert_json_success(response) self.assertEqual(json["url"], "example.com") @responses.activate def test_zoom_create_audio_call(self) -> None: responses.add( responses.POST, "https://zoom.example.com/oauth/token", json={"access_token": "token", "expires_in": 3599}, ) responses.add( responses.POST, self.user_zoom_meeting_url, json={"join_url": "example.com"}, ) response = self.client_post("/json/calls/zoom/create", {"is_video_call": "false"}) self.assertEqual( responses.calls[-1].request.url, self.user_zoom_meeting_url, ) assert responses.calls[-1].request.body is not None self.assertEqual( orjson.loads(responses.calls[-1].request.body), { "settings": { "host_video": False, "participant_video": False, }, "default_password": True, }, ) self.assertEqual( responses.calls[-1].request.headers["Authorization"], "Bearer token", ) json = self.assert_json_success(response) self.assertEqual(json["url"], "example.com") class BigBlueButtonVideoCallTest(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") self.login_user(self.user) self.signer = Signer() self.signed_bbb_a_object = self.signer.sign_object( { "meeting_id": "a", "name": "a", "lock_settings_disable_cam": True, "moderator": self.user.id, } ) # For testing viewer role (different creator / moderator from self) self.signed_bbb_a_object_different_creator = self.signer.sign_object( { "meeting_id": "a", "name": "a", "lock_settings_disable_cam": True, "moderator": self.example_user("cordelia").id, } ) def test_create_bigbluebutton_link(self) -> None: with ( mock.patch("zerver.views.video_calls.random.randint", return_value="1"), mock.patch("secrets.token_bytes", return_value=b"\x00" * 20), ): with mock.patch("zerver.views.video_calls.random.randint", return_value="1"): response = self.client_get( "/json/calls/bigbluebutton/create?meeting_name=general > meeting&voice_only=false" ) response_dict = self.assert_json_success(response) self.assertEqual( response_dict["url"], append_url_query_string( "/calls/bigbluebutton/join", "bigbluebutton=" + self.signer.sign_object( { "meeting_id": "zulip-1", "name": "general > meeting", "lock_settings_disable_cam": False, "moderator": self.user.id, } ), ), ) # Testing for audio call response = self.client_get( "/json/calls/bigbluebutton/create?meeting_name=general > meeting&voice_only=true" ) response_dict = self.assert_json_success(response) self.assertEqual( response_dict["url"], append_url_query_string( "/calls/bigbluebutton/join", "bigbluebutton=" + self.signer.sign_object( { "meeting_id": "zulip-1", "name": "general > meeting", "lock_settings_disable_cam": True, "moderator": self.user.id, } ), ), ) @responses.activate def test_join_bigbluebutton_redirect(self) -> None: responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True" "&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", "SUCCESS0", ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object} ) self.assertEqual(response.status_code, 302) self.assertEqual(isinstance(response, HttpResponseRedirect), True) self.assertEqual( response["Location"], "https://bbb.example.com/bigbluebutton/api/join?meetingID=a&" "role=MODERATOR&fullName=King%20Hamlet&createTime=0&checksum=54259b884a7c20ddcd7b280a1b62e59d7990568fe4f22001812bc4bcfd161a46", ) # Testing for viewer role response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object_different_creator}, ) self.assertEqual(response.status_code, 302) self.assertEqual(isinstance(response, HttpResponseRedirect), True) self.assertEqual( response["Location"], "https://bbb.example.com/bigbluebutton/api/join?meetingID=a&" "role=VIEWER&fullName=King%20Hamlet&createTime=0&checksum=52efaf64109ca4ec5a20a1d295f315af53f9e6ec30b50ed3707fd2909ac6bd94", ) @responses.activate def test_join_bigbluebutton_invalid_signature(self) -> None: responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True" "&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", "SUCCESS0", ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object + "zoo"} ) self.assert_json_error(response, "Invalid signature.") @responses.activate def test_join_bigbluebutton_redirect_wrong_big_blue_button_checksum(self) -> None: responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", "FAILEDchecksumError" "You did not pass the checksum security check", ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}, ) self.assert_json_error(response, "Error authenticating to the BigBlueButton server") @responses.activate def test_join_bigbluebutton_redirect_server_error(self) -> None: # Simulate bbb server error responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", status=500, body="Something went wrong", ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}, ) self.assert_json_error( response, "Error connecting to the BigBlueButton server: HTTP 500: Something went wrong" ) @responses.activate def test_join_bigbluebutton_connection_refused(self) -> None: responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", body=requests.exceptions.ConnectionError("Connection refused"), ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}, ) self.assert_json_error( response, "Error connecting to the BigBlueButton server: Connection refused" ) @responses.activate def test_join_bigbluebutton_redirect_error_by_server(self) -> None: # Simulate bbb server error responses.add( responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&lockSettingsDisableCam=True&checksum=33349e6374ca9b2d15a0c6e51a42bc3e8f770de13f88660815c6449859856e20", "FAILUREotherFailure", ) response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}, ) self.assert_json_error( response, "BigBlueButton server returned an unexpected error: FAILURE" ) def test_join_bigbluebutton_redirect_not_configured(self) -> None: with self.settings(BIG_BLUE_BUTTON_SECRET=None, BIG_BLUE_BUTTON_URL=None): response = self.client_get( "/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}, ) self.assert_json_error(response, "BigBlueButton credentials have not been configured") class ConstructorGroupsVideoCallTest(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") self.login_user(self.user_profile) self.base_api_url = "https://example.constructor.app/api/groups/xapi" self.test_room_guid = "a13b8686-d383-4763-b3b7-d2d4dd7f34ec" self.test_room_name = "King Hamlet's Zulip room" self.test_room_url = f"https://example.constructor.app/groups/room/{self.test_room_name}" self.mock_valid_room = { "room_guid": self.test_room_guid, "name": self.test_room_name, "url": self.test_room_url, } self.mock_room_missing_url = { "room_guid": self.test_room_guid, "name": self.test_room_name, } def test_missing_constructor_groups_settings(self) -> None: for setting_name in [ "CONSTRUCTOR_GROUPS_URL", "CONSTRUCTOR_GROUPS_ACCESS_KEY", "CONSTRUCTOR_GROUPS_SECRET_KEY", ]: with self.settings(**{setting_name: None}): response = self.client_post("/json/calls/constructorgroups/create") self.assert_json_error( response, "Constructor Groups credentials have not been configured" ) @responses.activate def test_get_existing_room(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", json=self.mock_valid_room, status=200, ) response = self.client_post("/json/calls/constructorgroups/create") response_dict = self.assert_json_success(response) self.assertEqual(response_dict["url"], self.test_room_url) self.assert_length(responses.calls, 1) @responses.activate def test_create_new_room(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", json=self.mock_valid_room, status=201, ) response = self.client_post("/json/calls/constructorgroups/create") response_dict = self.assert_json_success(response) self.assertEqual(response_dict["url"], self.test_room_url) self.assert_length(responses.calls, 1) @responses.activate def test_missing_url_in_response(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", json=self.mock_room_missing_url, status=200, ) with self.assertLogs(level="ERROR") as logs: response = self.client_post("/json/calls/constructorgroups/create") self.assert_json_error(response, "Failed to create Constructor Groups call") self.assertIn("Constructor Groups API returned room without URL", logs.output[0]) @responses.activate def test_http_request_error_handling(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", status=500, ) with self.assertLogs(level="ERROR") as error_log: response = self.client_post("/json/calls/constructorgroups/create") self.assertIn("Constructor Groups API request failed", error_log.output[0]) self.assert_json_error(response, "Failed to create Constructor Groups call") @responses.activate def test_api_response_json_error(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", body="invalid json", status=200, ) with self.assertLogs(level="ERROR") as error_log: response = self.client_post("/json/calls/constructorgroups/create") self.assertIn("Constructor Groups API request failed", error_log.output[0]) self.assert_json_error(response, "Failed to create Constructor Groups call") @responses.activate def test_request_contains_correct_data(self) -> None: responses.add( responses.POST, f"{self.base_api_url}/room/default", json=self.mock_valid_room, status=200, ) response = self.client_post("/json/calls/constructorgroups/create") self.assert_json_success(response) # Verify request body assert responses.calls[0].request.body is not None request_body = orjson.loads(responses.calls[0].request.body) self.assertEqual(request_body["creator_email"], self.user_profile.delivery_email) self.assertEqual(request_body["name"], f"{self.user_profile.full_name}'s Zulip room") self.assertEqual( request_body["fallback_name"], f"{self.user_profile.full_name}'s Zulip room ({self.user_profile.realm_id}-{self.user_profile.id})", ) def test_unsupported_http_method(self) -> None: from zerver.lib.exceptions import JsonableError from zerver.views.video_calls import ConstructorGroupsService service = ConstructorGroupsService() with self.assertLogs(level="ERROR") as error_log: with self.assertRaises(JsonableError) as cm: service._make_authenticated_request("GET", "/room/default") self.assertEqual(str(cm.exception.msg), "Failed to create Constructor Groups call") self.assertIn("Constructor Groups API request failed", error_log.output[0]) class NextcloudVideoCallTest(ZulipTestCase): @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") self.login_user(self.user) self.nextcloud_api_url = "https://nextcloud.example.com/ocs/v2.php/apps/spreed/api/v4/room" @responses.activate def test_create_nextcloud_talk_video_call_success(self) -> None: responses.add( responses.POST, self.nextcloud_api_url, json={ "ocs": { "meta": {"status": "ok", "statuscode": 200, "message": "OK"}, "data": { "token": "abc123token", "name": "Test Meeting", "displayName": "Test Meeting", "type": 3, }, } }, status=200, ) response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": "#Test > team check-in"} ) self.assertEqual(responses.calls[0].request.url, self.nextcloud_api_url) self.assertEqual( responses.calls[0].request.headers["OCS-APIRequest"], "true", ) self.assertEqual( responses.calls[0].request.headers["Accept"], "application/json", ) self.assertIn("Authorization", responses.calls[0].request.headers) self.assertTrue(responses.calls[0].request.headers["Authorization"].startswith("Basic ")) assert responses.calls[0].request.body is not None self.assertEqual( orjson.loads(responses.calls[0].request.body), { "roomType": 3, "roomName": "#Test > team check-in", }, ) json = self.assert_json_success(response) self.assertEqual(json["url"], "https://nextcloud.example.com/index.php/call/abc123token") def test_create_nextcloud_talk_not_configured(self) -> None: for setting_name in [ "NEXTCLOUD_SERVER", "NEXTCLOUD_TALK_USERNAME", "NEXTCLOUD_TALK_PASSWORD", ]: with self.settings(**{setting_name: None}): response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": "#Test > team check-in"}, ) self.assert_json_error( response, "Nextcloud Talk credentials have not been configured" ) @responses.activate def test_create_nextcloud_talk_server_error(self) -> None: responses.add( responses.POST, self.nextcloud_api_url, status=400, body="Invalid request", ) response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": "#Test > team check-in"} ) self.assert_json_error( response, "Error connecting to the Nextcloud Talk server: HTTP 400: Invalid request" ) @responses.activate def test_create_nextcloud_talk_connection_error(self) -> None: responses.add( responses.POST, self.nextcloud_api_url, body=requests.RequestException("Connection failed"), ) response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": "#Test > team check-in"} ) self.assert_json_error( response, "Error connecting to the Nextcloud Talk server: Connection failed" ) @responses.activate def test_create_nextcloud_talk_invalid_response(self) -> None: # Missing 'token' field responses.add( responses.POST, self.nextcloud_api_url, json={ "ocs": { "meta": {"status": "ok"}, "data": {}, } }, status=200, ) response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": "#Test > team check-in"}, ) self.assert_json_error(response, "Failed to create Nextcloud Talk call") @responses.activate def test_create_nextcloud_talk_truncates_long_room_name(self) -> None: responses.add( responses.POST, self.nextcloud_api_url, json={ "ocs": { "meta": {"status": "ok", "statuscode": 200, "message": "OK"}, "data": { "token": "abc123token", "name": "Truncated Conversation", "displayName": "Truncated Meeting", "type": 3, }, } }, status=200, ) long_room_name = "A" * 300 response = self.client_post( "/json/calls/nextcloud_talk/create", {"room_name": long_room_name} ) from zerver.views.video_calls import MAX_NEXTCLOUD_TALK_ROOM_NAME_LENGTH assert responses.calls[0].request.body is not None request_body = orjson.loads(responses.calls[0].request.body) self.assert_length(request_body["roomName"], MAX_NEXTCLOUD_TALK_ROOM_NAME_LENGTH) self.assertTrue(request_body["roomName"].endswith("...")) json = self.assert_json_success(response) self.assertEqual(json["url"], "https://nextcloud.example.com/index.php/call/abc123token")