def test_obtain_token(): api = API(base_url=TEST_BASE_URL) auth_result = api.obtain_token(**TEST_CREDENTIALS) assert "access_token" in auth_result assert len(auth_result["access_token"]) > 0 assert "user_id" in auth_result assert auth_result["user_id"] == TEST_CREDENTIALS["user_id"] assert "expires" in auth_result
def test_course_crud(): api = API(base_url=TEST_BASE_URL) # obtain token auth_result = api.obtain_token(**TEST_CREDENTIALS) api.access_token = auth_result["access_token"] # create course title = "SDK Course" lti_context_id = generate_ctx_id() lti_tool_consumer_instance_guid = "test.institution.edu" course_created = api.create_course( title=title, lti_context_id=lti_context_id, lti_tool_consumer_instance_guid=lti_tool_consumer_instance_guid, ) assert "id" in course_created assert course_created["title"] == title course_id = course_created["id"] # get course by its primary key course_read = api.get_course(course_id) assert "id" in course_read assert course_read["id"] == course_id # find course by context id and instance guid # exact match expected because these two attributes uniquely identify a course courses_found = api.list_courses( lti_context_id=lti_context_id, lti_tool_consumer_instance_guid=lti_tool_consumer_instance_guid) assert len(courses_found) == 1 assert courses_found[0]["id"] == course_id # search courses by title, which should include the course that was just created courses_search = api.search_courses(text=title) assert len(courses_search) > 0 assert course_id in [course["id"] for course in courses_search] # update course details update_params = dict( title=title + " Updated", sis_course_id="test123", canvas_course_id=100, lti_context_title="My Test Context", lti_context_label="TCX", lti_context_id=course_created[ "lti_context_id"], # TODO: why is this required for an update? lti_tool_consumer_instance_guid=course_created[ "lti_tool_consumer_instance_guid"], # TODO: why is this required for an update? ) course_updated = api.update_course(course_id, **update_params) course_updated_subset = {k: course_updated[k] for k in update_params} assert update_params == course_updated_subset # delete the course course_deleted = api.delete_course(course_id) assert course_deleted == {}
def test_obtain_token_with_invalid_course_permission(): api = API(base_url=TEST_BASE_URL) with pytest.raises(ValueError): api.obtain_token( client_id=TEST_CLIENT_ID, client_secret=TEST_CLIENT_SECRET, user_id=TEST_USER_ID, course_permission="invalid", )
def __init__(self, client_id: str, client_secret: str, base_url: str): if client_id is None or client_secret is None: raise ValueError("Missing client credentials") if base_url is None: raise ValueError("Missing base URL to use for API requests") self.client_id = client_id self.client_secret = client_secret self.base_url = base_url self.api = API(base_url=base_url)
def test_upload_multiple_images_and_add_to_collection(): api = API(base_url=TEST_BASE_URL) auth_result = api.obtain_token(**TEST_CREDENTIALS) api.access_token = auth_result["access_token"] # create course course_created = api.create_course( title="SDK Course", lti_context_id=generate_ctx_id(), lti_tool_consumer_instance_guid="test.institution.edu", ) course_id = course_created["id"] # upload multiple images to the course image_ids = [] content_type = "image/png" upload_files = [(file_name, open(os.path.join(TEST_IMAGES_DIR, file_name), "rb"), content_type) for file_name in ( "16x16-ff00ff7f.png", "32x32-faaa1aff.png", "64x64-ff0000c1.png", )] try: images_uploaded = api.upload_images( course_id, upload_files=upload_files, title="Color Block", ) assert len( images_uploaded) == 3, "list returned with the uploaded image" image_ids = [image["id"] for image in images_uploaded] finally: for (file_name, fp, content_type) in upload_files: fp.close() # create collection collection_created = api.create_collection(course_id, title="SDK Course", description="Just a test") collection_id = collection_created["id"] # add images to collection update_params = dict( course_id=course_id, title=collection_created["title"], course_image_ids=image_ids, ) collection_updated = api.update_collection(collection_id, **update_params) assert collection_updated["course_image_ids"] == image_ids # delete course api.delete_course(course_id)
def test_api_implements_required_methods(): methods = ( "obtain_token", "list_courses", "search_courses", "get_course", "create_course", "update_course", "delete_course", "copy_course", "list_collections", "get_collection", "get_collection_images", "create_collection", "update_collection", "delete_collection", "upload_image", "upload_images", "update_image", "get_image", "delete_image", ) api = API() for method in methods: assert hasattr(api, method) assert callable(getattr(api, method))
def test_do_request_raises_exception_for_status(http_method, status_code, error_class): expected_url = f"{TEST_BASE_URL}/test/123" mock_resp = Mock() mock_resp.status_code = status_code mock_resp.raise_for_status = Mock(side_effect=error_class) mock_resp.json = Mock(return_value=None) with patch(f"requests.{http_method}", return_value=mock_resp) as mock_method: with pytest.raises(error_class): API()._do_request(method=http_method, url=expected_url)
def test_collection_crud(): api = API(base_url=TEST_BASE_URL) auth_result = api.obtain_token(**TEST_CREDENTIALS) api.access_token = auth_result["access_token"] # create course title = "SDK Course" course_created = api.create_course( title=title, lti_context_id=generate_ctx_id(), lti_tool_consumer_instance_guid="test.institution.edu", ) course_id = course_created["id"] # create collection collection_params = dict( title="SDK Course", description="This is a fascinating collection", ) collection_created = api.create_collection(course_id, **collection_params) assert "id" in collection_created assert collection_created["title"] == collection_params["title"] assert collection_created["description"] == collection_params[ "description"] collection_id = collection_created["id"] # update collection update_params = dict( course_id=course_id, # TODO: why is this required for an update? title=collection_created["title"] + " Updated", description=collection_created["description"] + " Updated", ) collection_updated = api.update_collection(collection_id, **update_params) collection_updated_subset = { k: collection_updated[k] for k in update_params } assert update_params == collection_updated_subset # delete collection collection_deleted = api.delete_collection(collection_id) assert collection_deleted == {} # delete course course_deleted = api.delete_course(course_id) assert course_deleted == {}
def test_obtain_token_for_user(): headers = TEST_HEADERS data = { "client_id": TEST_CLIENT_ID, "client_secret": TEST_CLIENT_SECRET, "user_id": TEST_USER_ID, } expected_token = "b0a4e9e4ae4a4cbcb079eab3637f2b22" api = API(base_url=TEST_BASE_URL) api._do_request = Mock(return_value={"access_token": expected_token}) actual_response = api.obtain_token(client_id=TEST_CLIENT_ID, client_secret=TEST_CLIENT_SECRET, user_id=TEST_USER_ID) api._do_request.assert_called_with( method="post", url=f"{TEST_BASE_URL}/auth/obtain-token", headers=headers, json=data, ) assert actual_response["access_token"] == expected_token
def test_do_request_with_method(http_method, status_code): url = f"{TEST_BASE_URL}/test/123" expected_response = {"id": "100"} mock_resp = Mock() mock_resp.status_code = status_code mock_resp.json = Mock(return_value=expected_response) with patch(f"requests.{http_method}", return_value=mock_resp) as mock_method: actual_response = API()._do_request(method=http_method, url=url) mock_method.assert_called_once_with(url, timeout=DEFAULT_TIMEOUT) assert actual_response == expected_response
def test_list_courses_filtered_by_lti_params(courses_fixture): course = courses_fixture[0] access_token = "token123" headers = dict(**TEST_HEADERS, Authorization=f"Token {access_token}") params = dict( lti_context_id=course["lti_context_id"], lti_tool_consumer_instance_guid=course[ "lti_tool_consumer_instance_guid"], canvas_course_id=None, sis_course_id=None, title=None, ) api = API(base_url=TEST_BASE_URL, access_token=access_token) api._do_request = Mock(return_value=[course]) actual_response = api.list_courses(**params) api._do_request.assert_called_with(method="get", url=f"{TEST_BASE_URL}/courses", headers=headers, params=params) assert actual_response == [course]
def test_image_crud(): api = API(base_url=TEST_BASE_URL) auth_result = api.obtain_token(**TEST_CREDENTIALS) api.access_token = auth_result["access_token"] # create course course_created = api.create_course( title="SDK Course", lti_context_id=generate_ctx_id(), lti_tool_consumer_instance_guid="test.institution.edu", ) course_id = course_created["id"] # upload a single image to the course image_id = None file_name = "16x16-ff00ff7f.png" content_type = "image/png" with open(os.path.join(TEST_IMAGES_DIR, file_name), "rb") as upload_file: image_uploaded = api.upload_image( course_id, upload_file=upload_file, file_name=file_name, content_type=content_type, title="Color Block Image", ) assert len( image_uploaded) == 1, "list returned with the uploaded image" assert "id" in image_uploaded[0], "the image has an id" image_id = image_uploaded[0]["id"] # get image details image_read = api.get_image(image_id) assert image_read["id"] == image_id # update image details update_params = dict( course_id=course_id, title=image_read["title"] + " Updated", description="Color block is amazing!", metadata=dict(Creator="SDK", Audience="Test", Date="2000"), ) image_updated = api.update_image(image_id, **update_params) assert image_updated["id"] == image_id assert image_updated["title"] == update_params["title"] # delete image image_deleted = api.delete_image(image_id) assert image_deleted == {} # delete course api.delete_course(course_id)
class Client(object): """ Client is a wrapper for interacting with the API. """ def __init__(self, client_id: str, client_secret: str, base_url: str): if client_id is None or client_secret is None: raise ValueError("Missing client credentials") if base_url is None: raise ValueError("Missing base URL to use for API requests") self.client_id = client_id self.client_secret = client_secret self.base_url = base_url self.api = API(base_url=base_url) def authenticate( self, user_id: str, course_id: Optional[int] = None, course_permission: Optional[str] = None, ) -> None: """ Authenticate with the API by supplying client credentials and obtaining a token tied to the user. """ if not user_id: raise ValueError("User ID is required to authenticate") response = self.api.obtain_token( client_id=self.client_id, client_secret=self.client_secret, user_id=user_id, course_id=course_id, course_permission=course_permission, ) self.api.access_token = response["access_token"] def find_or_create_course( self, lti_context_id: str, lti_tool_consumer_instance_guid: str, title: str = "", lti_context_title: Optional[str] = None, lti_context_label: Optional[str] = None, sis_course_id: Optional[str] = None, canvas_course_id: Optional[int] = None, ) -> dict: """ Helper method to find or create a course. """ courses = self.api.list_courses( lti_context_id=lti_context_id, lti_tool_consumer_instance_guid=lti_tool_consumer_instance_guid, ) if len(courses) > 1: raise ApiError("Multiple courses found") elif len(courses) == 1: return courses[0] return self.api.create_course( title=title, lti_context_id=lti_context_id, lti_tool_consumer_instance_guid=lti_tool_consumer_instance_guid, lti_context_title=lti_context_title, lti_context_label=lti_context_label, sis_course_id=sis_course_id, canvas_course_id=canvas_course_id, )