def test_zoom_api_request_missing_creds(): utils.common.APIGEE_KEY = None # (ZOOM_API_KEY, ZOOM_API_SECRET) cases = [(None, None), ("key", None), (None, "secret")] for key, secret in cases: utils.common.ZOOM_API_KEY = key utils.common.ZOOM_API_SECRET = secret with pytest.raises(Exception) as exc_info: utils.zoom_api_request("meetings") assert exc_info.match("Missing api credentials.")
def test_apigee_key(caplog): utils.common.APIGEE_KEY = "apigee_key" utils.common.ZOOM_API_KEY = None utils.common.ZOOM_API_SECRET = None caplog.set_level(logging.INFO) utils.zoom_api_request("meetings") assert "apigee request" in caplog.text.lower() # Should still use apigee key even if zoom api key/secret defined utils.common.ZOOM_API_KEY = "key" utils.common.ZOOM_API_SECRET = "secret" utils.zoom_api_request("meetings") assert "apigee request" in caplog.text.lower()
def test_zoom_api_key(caplog): utils.common.APIGEE_KEY = None utils.common.ZOOM_API_KEY = "key" utils.common.ZOOM_API_SECRET = "secret" caplog.set_level(logging.INFO) utils.zoom_api_request("meetings") assert "zoom api request" in caplog.text.lower() utils.common.APIGEE_KEY = "" utils.common.ZOOM_API_KEY = "key" utils.common.ZOOM_API_SECRET = "secret" caplog.set_level(logging.INFO) utils.zoom_api_request("meetings") assert "zoom api request" in caplog.text.lower()
def test_url_construction(caplog): utils.common.APIGEE_KEY = None utils.common.ZOOM_API_KEY = "key" utils.common.ZOOM_API_SECRET = "secret" caplog.set_level(logging.INFO) cases = [ ("https://www.foo.com", "meetings", "https://www.foo.com/meetings"), ("https://www.foo.com/", "meetings", "https://www.foo.com/meetings"), ("https://www.foo.com/", "/meetings", "https://www.foo.com/meetings"), ] with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, status_code=200, json={"mock_payload": 123}) for url, endpoint, expected in cases: utils.common.ZOOM_API_BASE_URL = url utils.zoom_api_request(endpoint) assert ("zoom api request to https://www.foo.com/meetings" in caplog.text.lower())
def test_zoom_api_request_success(): # test successful call cases = [ (None, "zoom_key", "zoom_secret"), ("", "zoom_key", "zoom_secret"), ] for apigee_key, zoom_key, zoom_secret in cases: utils.common.APIGEE_KEY = apigee_key utils.common.ZOOM_API_KEY = zoom_key utils.common.ZOOM_API_SECRET = zoom_secret with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, status_code=200, json={"mock_payload": 123}) r = utils.zoom_api_request("meetings") assert "mock_payload" in r.json()
def test_zoom_api_request_failures(): utils.common.APIGEE_KEY = None utils.common.ZOOM_API_KEY = "zoom_key" utils.common.ZOOM_API_SECRET = "zoom_secret" utils.common.ZOOM_API_BASE_URL = "https://api.zoom.us/v2/" # test failed call that returns with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, status_code=400, json={"mock_payload": 123}) r = utils.zoom_api_request("meetings", ignore_failure=True) assert r.status_code == 400 # test failed call that raises with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, status_code=400, json={"mock_payload": 123}) error_msg = "400 Client Error" with pytest.raises(requests.exceptions.HTTPError, match=error_msg): utils.zoom_api_request("meetings", ignore_failure=False, retries=0) # test ConnectionError handling with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, exc=requests.exceptions.ConnectionError) error_msg = "Error requesting https://api.zoom.us/v2/meetings" with pytest.raises(utils.common.ZoomApiRequestError, match=error_msg): utils.zoom_api_request("meetings") # test ConnectTimeout handling with requests_mock.mock() as req_mock: req_mock.get(requests_mock.ANY, exc=requests.exceptions.ConnectTimeout) error_msg = "Error requesting https://api.zoom.us/v2/meetings" with pytest.raises(utils.common.ZoomApiRequestError, match=error_msg): utils.zoom_api_request("meetings")
def test_zoom_api_request_missing_endpoint(): with pytest.raises(Exception) as exc_info: utils.zoom_api_request(endpoint=None) assert exc_info.match("Call to zoom_api_request missing endpoint")
def handler(event, context): """ This function acts as a relay to the traditional zoom webhook. The webhook function is called by Zoom on a "recording.completed" event, along with data about the recordings. Here we fetch the recording data from the zoom API. The response to that API call is (mostly) identical to the payload zoom sends to the webhook, so we can simply pass it along in our own webhook request, using a "on.demand.ingest" event type and including the series id as "on_demand_series_id". """ logger.info(event) if "body" not in event or event["body"] is None: return resp(400, "Bad data. No body found in event.") try: body = json.loads(event["body"]) logger.info({"on-demand request": body}) except json.JSONDecodeError: return resp(400, "Webhook notification body is not valid json.") if "uuid" not in body: return resp( 400, "Missing recording uuid field in webhook notification body.", ) recording_uuid = body["uuid"] if recording_uuid.startswith("https"): # it's a link; parse the uuid from the query string parsed_url = urlparse(recording_uuid) query_params = parse_qs(parsed_url.query) if "meeting_id" not in query_params: return resp( 404, "Zoom URL is malformed or missing 'meeting_id' param.", ) recording_uuid = query_params["meeting_id"][0] logger.info(f"Got recording uuid: '{recording_uuid}'") try: try: # zoom api can break if uuid is not double urlencoded double_urlencoded_uuid = quote( quote(recording_uuid, safe=""), safe="", ) zoom_endpoint = ( f"meetings/{double_urlencoded_uuid}/recordings" "?include_fields=download_access_token&ttl=3600" ) r = zoom_api_request(zoom_endpoint) recording_data = r.json() except requests.HTTPError as e: # return a 404 if there's no such meeting if e.response.status_code == 404: return resp( 404, f"No zoom recording with id '{recording_uuid}'" ) else: raise # otherwise return a 500 on any other errors (bad json, bad request, etc) except Exception as e: return resp( 500, f"Something went wrong querying the zoom api: {str(e)}", ) if ( "recording_files" not in recording_data or not recording_data["recording_files"] ): return resp( 503, "Zoom api response contained no recording files for " f"{recording_uuid}", ) # verify that all the recording files are actually "completed" not_completed = sum( 1 for x in recording_data["recording_files"] if x.get("status") and x.get("status") != "completed" ) if not_completed > 0: return resp( 503, "Not all recorded files have status 'completed'", ) webhook_data = { "event": "on.demand.ingest", "payload": { "object": recording_data, }, "download_token": recording_data["download_access_token"], } # series id is an optional param. if not present the download function will # attempt to determine the series id by matching the recording times # against it's known schedule as usual. if "oc_series_id" in body and body["oc_series_id"]: webhook_data["payload"]["on_demand_series_id"] = body["oc_series_id"] if "allow_multiple_ingests" in body: webhook_data["payload"]["allow_multiple_ingests"] = body[ "allow_multiple_ingests" ] if "oc_workflow" in body: webhook_data["payload"]["oc_workflow"] = body["oc_workflow"] if "ingest_all_mp4" in body: webhook_data["payload"]["ingest_all_mp4"] = body["ingest_all_mp4"] zip_id = f"on-demand-{str(uuid4())}" webhook_data["payload"]["zip_id"] = zip_id logger.info({"webhook_data": webhook_data}) try: r = requests.post( WEBHOOK_ENDPOINT_URL, data=json.dumps(webhook_data), headers={ "Content-type": "application/json", }, ) r.raise_for_status() if r.status_code == 204: raise Exception("Webhook returned 204: ingest not accepted") except Exception as e: err_msg = str(e) logger.exception( f"Something went wrong calling the webhook: {err_msg}" ) return resp(500, err_msg) set_pipeline_status( zip_id, PipelineStatus.ON_DEMAND_RECEIVED, meeting_id=webhook_data["payload"]["object"]["id"], recording_id=recording_uuid, recording_start_time=webhook_data["payload"]["object"]["start_time"], topic=webhook_data["payload"]["object"]["topic"], origin="on_demand", oc_series_id=body.get("oc_series_id"), ) return resp(200, "Ingest accepted")
def host_name(self): if not hasattr(self, "_host_name"): resp = zoom_api_request(f"users/{self.data['host_id']}").json() logger.info({"Host details": resp}) self._host_name = f"{resp['first_name']} {resp['last_name']}" return self._host_name