From f4f316d8c8904276abde1996cf5f85a99db868f2 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Wed, 6 Nov 2024 15:50:21 +0100 Subject: [PATCH 01/16] get_events: tests for get_events with multiple filter combinations --- tests/test_server.py | 302 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/tests/test_server.py b/tests/test_server.py index ce5a98d07..b1410c338 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,10 +11,14 @@ is_connection_created, close_connection, get, + get_events, + get_project_names, + get_user, get_server_api_connection, get_base_url, get_rest_url, ) +from ayon_api import exceptions AYON_BASE_URL = os.getenv("AYON_SERVER_URL") AYON_REST_URL = "{}/api".format(AYON_BASE_URL) @@ -43,3 +47,301 @@ def test_get(): res = get("info") assert res.status_code == 200 assert isinstance(res.data, dict) + + +test_project_names = [ + # (None), + # ([]), + (["demo_Big_Episodic"]), + (["demo_Big_Feature"]), + (["demo_Commercial"]), + (["AY_Tests"]), + (["demo_Big_Episodic", "demo_Big_Feature", "demo_Commercial", "AY_Tests"]) +] + +test_topics = [ + # (None), + # ([]), + (["entity.folder.attrib_changed"]), + (["entity.task.created", "entity.project.created"]), + (["settings.changed", "entity.version.status_changed"]), + (["entity.task.status_changed", "entity.folder.deleted"]), + # (["entity.project.changed", "entity.task.tags_changed", "entity.product.created"]) +] + +test_users = [ + # (None), + # ([]), + (["admin"]), + (["mkolar", "tadeas.8964"]), + # (["roy", "luke.inderwick", "ynbot"]), + # (["entity.folder.attrib_changed", "entity.project.created", "entity.task.created", "settings.changed"]), +] + +# incorrect name for statuses +test_states = [ + # (None), + # ([]), + (["pending", "in_progress", "finished", "failed", "aborted", "restarted"]), + # (["failed", "aborted"]), + # (["pending", "in_progress"]), + # (["finished", "failed", "restarted"]), + (["finished"]), +] + +test_include_logs = [ + (None), + (True), + (False), +] + +test_has_children = [ + (None), + (True), + (False), +] + +from datetime import datetime, timedelta + +test_newer_than = [ + (None), + ((datetime.now() - timedelta(days=2)).isoformat()), + ((datetime.now() - timedelta(days=5)).isoformat()), + # ((datetime.now() - timedelta(days=10)).isoformat()), + # ((datetime.now() - timedelta(days=20)).isoformat()), + # ((datetime.now() - timedelta(days=30)).isoformat()), +] + +test_older_than = [ + (None), + ((datetime.now() - timedelta(days=0)).isoformat()), + ((datetime.now() - timedelta(days=0)).isoformat()), + # ((datetime.now() - timedelta(days=5)).isoformat()), + # ((datetime.now() - timedelta(days=10)).isoformat()), + # ((datetime.now() - timedelta(days=20)).isoformat()), + # ((datetime.now() - timedelta(days=30)).isoformat()), +] + +test_fields = [ + (None), + ([]), +] + + +@pytest.mark.parametrize("topics", test_topics) +@pytest.mark.parametrize("project_names", test_project_names) +@pytest.mark.parametrize("states", test_states) +@pytest.mark.parametrize("users", test_users) +@pytest.mark.parametrize("include_logs", test_include_logs) +@pytest.mark.parametrize("has_children", test_has_children) +@pytest.mark.parametrize("newer_than", test_newer_than) +@pytest.mark.parametrize("older_than", test_older_than) +@pytest.mark.parametrize("fields", test_fields) +def test_get_events_all_filter_combinations( + topics, + project_names, + states, + users, + include_logs, + has_children, + newer_than, + older_than, + fields): + """Tests all combination of possible filters. + """ + res = get_events( + topics=topics, + project_names=project_names, + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + ) + + list_res = list(res) + + for item in list_res: + assert item.get("topic") in topics, ( + f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" + ) + assert item.get("project") in project_names, ( + f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" + ) + assert item.get("user") in users, ( + f"Expected 'user' one of values: {users}, but got '{item.get('user')}'" + ) + assert item.get("status") in states, ( + f"Expected 'state' to be one of {states}, but got '{item.get('state')}'" + ) + assert (newer_than is None) or ( + datetime.fromisoformat(item.get("createdAt") > datetime.fromisoformat(newer_than)) + ) + assert (older_than is None) or ( + datetime.fromisoformat(item.get("createdAt") < datetime.fromisoformat(older_than)) + ) + + assert topics is None or len(list_res) == sum(len(list(get_events( + topics=[topic], + project_names=project_names, + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields) + )) for topic in topics) + + assert project_names is None or len(list_res) == sum(len(list(get_events( + topics=topics, + project_names=[project_name], + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields) + )) for project_name in project_names) + + assert states is None or len(list_res) == sum(len(list(get_events( + topics=topics, + project_names=project_names, + states=[state], + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields) + )) for state in states) + + assert users is None or len(list_res) == sum(len(list(get_events( + topics=topics, + project_names=project_names, + states=states, + users=[user], + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields) + )) for user in users) + + assert fields is None or len(list_res) == sum(len(list(get_events( + topics=topics, + project_names=project_names, + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=[field]) + )) for field in fields) + + +######################## +# topics=None, event_ids=None, project_names=None, states=None, users=None, include_logs=None, has_children=None, newer_than=None, older_than=None, fields=None + +# [ +# { +# 'description': 'Changed task animation status to In progress', +# 'hash': 'a259521612b611ef95920242c0a81005', +# 'project': 'demo_Big_Episodic', +# 'id': 'a259521612b611ef95920242c0a81005', +# 'status': 'finished', +# 'user': 'admin', +# 'createdAt': '2024-05-15T14:28:28.889144+02:00', +# 'dependsOn': None, +# 'updatedAt': '2024-05-15T14:28:28.889144+02:00', +# 'retries': 0, +# 'sender': 'wWN64PyUo1kqAxechtJucy', +# 'topic': 'entity.task.status_changed' +# }, +# { +# 'description': 'Changed task animation status to On hold', +# 'hash': 'a8fb977812b611ef95920242c0a81005', +# 'project': 'demo_Big_Episodic', +# 'id': 'a8fb977812b611ef95920242c0a81005', +# 'status': 'finished', +# 'user': 'admin', +# 'createdAt': '2024-05-15T14:28:40.018934+02:00', +# 'dependsOn': None, +# 'updatedAt': '2024-05-15T14:28:40.018934+02:00', +# 'retries': 0, +# 'sender': 'fx5SG26FHvhFKkDsXHp53k', +# 'topic': 'entity.task.status_changed' +# }, +# { +# 'description': 'Changed task animation status to Pending review', +# 'hash': 'f0686ec412b611ef95920242c0a81005', +# 'project': 'demo_Big_Episodic', +# 'id': 'f0686ec412b611ef95920242c0a81005', +# 'status': 'finished', +# 'user': 'admin', +# 'createdAt': '2024-05-15T14:30:39.850258+02:00', +# 'dependsOn': None, +# 'updatedAt': '2024-05-15T14:30:39.850258+02:00', +# 'retries': 0, +# 'sender': 'v9ciM94XnfJ33X1bYr5ESv', +# 'topic': 'entity.task.status_changed' +# } +# ] + + +@pytest.mark.parametrize("project_names", test_project_names) +def test_get_events_project_name(project_names): + res = get_events(project_names=project_names) + + list_res = list(res) + + users = set() + for item in list_res: + users.add(item.get("user")) + assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" + + print(users) + # test if the legths are equal + assert len(list_res) == sum(len(list(get_events(project_names=[project_name]))) for project_name in project_names) + + +@pytest.mark.parametrize("project_names", test_project_names) +@pytest.mark.parametrize("topics", test_topics) +def test_get_events_project_name_topic(project_names, topics): + print(project_names, "", topics) + res = get_events(topics=topics, project_names=project_names) + + list_res = list(res) + + for item in list_res: + assert item.get("topic") in topics + assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" + + # test if the legths are equal + assert len(list_res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) + assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + + +@pytest.mark.parametrize("project_names", test_project_names) +@pytest.mark.parametrize("topics", test_topics) +@pytest.mark.parametrize("users", test_users) +def test_get_events_project_name_topic_user(project_names, topics, users): + # print(project_names, "", topics) + res = get_events(topics=topics, project_names=project_names, users=users) + + list_res = list(res) + + for item in list_res: + assert item.get("topic") in topics, f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" + assert item.get("project") in project_names, f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" + assert item.get("user") in project_names, f"Expected 'project' one of values: {users}, but got '{item.get('user')}'" + + + # test if the legths are equal + assert len(list_res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) + assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) From da5801ef82696f38f34c5cfc9867d58204c948c9 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 7 Nov 2024 10:14:32 +0100 Subject: [PATCH 02/16] get_events: solved issue with connection timeout while testing, new tests added and reduced the number of combinations in all filter combination test --- tests/test_server.py | 154 ++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 89 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index b1410c338..a20439ff7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -17,6 +17,8 @@ get_server_api_connection, get_base_url, get_rest_url, + get_timeout, + set_timeout ) from ayon_api import exceptions @@ -50,8 +52,8 @@ def test_get(): test_project_names = [ - # (None), - # ([]), + (None), + ([]), (["demo_Big_Episodic"]), (["demo_Big_Feature"]), (["demo_Commercial"]), @@ -60,32 +62,32 @@ def test_get(): ] test_topics = [ - # (None), - # ([]), + (None), + ([]), (["entity.folder.attrib_changed"]), (["entity.task.created", "entity.project.created"]), (["settings.changed", "entity.version.status_changed"]), (["entity.task.status_changed", "entity.folder.deleted"]), - # (["entity.project.changed", "entity.task.tags_changed", "entity.product.created"]) + (["entity.project.changed", "entity.task.tags_changed", "entity.product.created"]) ] test_users = [ - # (None), - # ([]), + (None), + ([]), (["admin"]), (["mkolar", "tadeas.8964"]), - # (["roy", "luke.inderwick", "ynbot"]), - # (["entity.folder.attrib_changed", "entity.project.created", "entity.task.created", "settings.changed"]), + (["roy", "luke.inderwick", "ynbot"]), + (["entity.folder.attrib_changed", "entity.project.created", "entity.task.created", "settings.changed"]), ] # incorrect name for statuses test_states = [ - # (None), - # ([]), + (None), + ([]), (["pending", "in_progress", "finished", "failed", "aborted", "restarted"]), - # (["failed", "aborted"]), - # (["pending", "in_progress"]), - # (["finished", "failed", "restarted"]), + (["failed", "aborted"]), + (["pending", "in_progress"]), + (["finished", "failed", "restarted"]), (["finished"]), ] @@ -101,25 +103,25 @@ def test_get(): (False), ] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone test_newer_than = [ (None), - ((datetime.now() - timedelta(days=2)).isoformat()), - ((datetime.now() - timedelta(days=5)).isoformat()), - # ((datetime.now() - timedelta(days=10)).isoformat()), - # ((datetime.now() - timedelta(days=20)).isoformat()), - # ((datetime.now() - timedelta(days=30)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=2)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=5)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=10)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=20)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=30)).isoformat()), ] test_older_than = [ (None), - ((datetime.now() - timedelta(days=0)).isoformat()), - ((datetime.now() - timedelta(days=0)).isoformat()), - # ((datetime.now() - timedelta(days=5)).isoformat()), - # ((datetime.now() - timedelta(days=10)).isoformat()), - # ((datetime.now() - timedelta(days=20)).isoformat()), - # ((datetime.now() - timedelta(days=30)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=0)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=0)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=5)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=10)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=20)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=30)).isoformat()), ] test_fields = [ @@ -128,15 +130,16 @@ def test_get(): ] -@pytest.mark.parametrize("topics", test_topics) -@pytest.mark.parametrize("project_names", test_project_names) -@pytest.mark.parametrize("states", test_states) -@pytest.mark.parametrize("users", test_users) -@pytest.mark.parametrize("include_logs", test_include_logs) -@pytest.mark.parametrize("has_children", test_has_children) -@pytest.mark.parametrize("newer_than", test_newer_than) -@pytest.mark.parametrize("older_than", test_older_than) -@pytest.mark.parametrize("fields", test_fields) +# takes max 3 items in a list to reduce the number of combinations +@pytest.mark.parametrize("topics", test_topics[-3:]) +@pytest.mark.parametrize("project_names", test_project_names[-3:]) +@pytest.mark.parametrize("states", test_states[-3:]) +@pytest.mark.parametrize("users", test_users[-3:]) +@pytest.mark.parametrize("include_logs", test_include_logs[-3:]) +@pytest.mark.parametrize("has_children", test_has_children[-3:]) +@pytest.mark.parametrize("newer_than", test_newer_than[-3:]) +@pytest.mark.parametrize("older_than", test_older_than[-3:]) +@pytest.mark.parametrize("fields", test_fields[-3:]) def test_get_events_all_filter_combinations( topics, project_names, @@ -146,9 +149,15 @@ def test_get_events_all_filter_combinations( has_children, newer_than, older_than, - fields): + fields +): """Tests all combination of possible filters. """ + # with many tests - ayon_api.exceptions.ServerError: Connection timed out. + # TODO - maybe some better solution + if get_timeout() < 5: + set_timeout(10.0) + res = get_events( topics=topics, project_names=project_names, @@ -163,6 +172,7 @@ def test_get_events_all_filter_combinations( list_res = list(res) + # test if filtering was correct for item in list_res: assert item.get("topic") in topics, ( f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" @@ -177,12 +187,13 @@ def test_get_events_all_filter_combinations( f"Expected 'state' to be one of {states}, but got '{item.get('state')}'" ) assert (newer_than is None) or ( - datetime.fromisoformat(item.get("createdAt") > datetime.fromisoformat(newer_than)) + datetime.fromisoformat(item.get("createdAt")) > datetime.fromisoformat(newer_than) ) assert (older_than is None) or ( - datetime.fromisoformat(item.get("createdAt") < datetime.fromisoformat(older_than)) + datetime.fromisoformat(item.get("createdAt")) < datetime.fromisoformat(older_than) ) + # test if all events were given assert topics is None or len(list_res) == sum(len(list(get_events( topics=[topic], project_names=project_names, @@ -242,55 +253,6 @@ def test_get_events_all_filter_combinations( older_than=older_than, fields=[field]) )) for field in fields) - - -######################## -# topics=None, event_ids=None, project_names=None, states=None, users=None, include_logs=None, has_children=None, newer_than=None, older_than=None, fields=None - -# [ -# { -# 'description': 'Changed task animation status to In progress', -# 'hash': 'a259521612b611ef95920242c0a81005', -# 'project': 'demo_Big_Episodic', -# 'id': 'a259521612b611ef95920242c0a81005', -# 'status': 'finished', -# 'user': 'admin', -# 'createdAt': '2024-05-15T14:28:28.889144+02:00', -# 'dependsOn': None, -# 'updatedAt': '2024-05-15T14:28:28.889144+02:00', -# 'retries': 0, -# 'sender': 'wWN64PyUo1kqAxechtJucy', -# 'topic': 'entity.task.status_changed' -# }, -# { -# 'description': 'Changed task animation status to On hold', -# 'hash': 'a8fb977812b611ef95920242c0a81005', -# 'project': 'demo_Big_Episodic', -# 'id': 'a8fb977812b611ef95920242c0a81005', -# 'status': 'finished', -# 'user': 'admin', -# 'createdAt': '2024-05-15T14:28:40.018934+02:00', -# 'dependsOn': None, -# 'updatedAt': '2024-05-15T14:28:40.018934+02:00', -# 'retries': 0, -# 'sender': 'fx5SG26FHvhFKkDsXHp53k', -# 'topic': 'entity.task.status_changed' -# }, -# { -# 'description': 'Changed task animation status to Pending review', -# 'hash': 'f0686ec412b611ef95920242c0a81005', -# 'project': 'demo_Big_Episodic', -# 'id': 'f0686ec412b611ef95920242c0a81005', -# 'status': 'finished', -# 'user': 'admin', -# 'createdAt': '2024-05-15T14:30:39.850258+02:00', -# 'dependsOn': None, -# 'updatedAt': '2024-05-15T14:30:39.850258+02:00', -# 'retries': 0, -# 'sender': 'v9ciM94XnfJ33X1bYr5ESv', -# 'topic': 'entity.task.status_changed' -# } -# ] @pytest.mark.parametrize("project_names", test_project_names) @@ -304,7 +266,6 @@ def test_get_events_project_name(project_names): users.add(item.get("user")) assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" - print(users) # test if the legths are equal assert len(list_res) == sum(len(list(get_events(project_names=[project_name]))) for project_name in project_names) @@ -340,8 +301,23 @@ def test_get_events_project_name_topic_user(project_names, topics, users): assert item.get("project") in project_names, f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" assert item.get("user") in project_names, f"Expected 'project' one of values: {users}, but got '{item.get('user')}'" - # test if the legths are equal assert len(list_res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + + +@pytest.mark.parametrize("newer_than", test_newer_than) +@pytest.mark.parametrize("older_than", test_older_than) +def test_get_events_timestamp(newer_than, older_than): + res = get_events(newer_than=newer_than, older_than=older_than) + + list_res = list(res) + + for item in list_res: + assert (newer_than is None) or ( + datetime.fromisoformat(item.get("createdAt") > datetime.fromisoformat(newer_than)) + ) + assert (older_than is None) or ( + datetime.fromisoformat(item.get("createdAt") < datetime.fromisoformat(older_than)) + ) From 20dd3253db4ebae1f6e315117a7f6906a5533c33 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 7 Nov 2024 13:58:13 +0100 Subject: [PATCH 03/16] get_events: New test for invalid names data, small code adjustments --- tests/test_server.py | 164 ++++++++++++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 41 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index a20439ff7..0ed6b7172 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -13,21 +13,20 @@ get, get_events, get_project_names, - get_user, + get_user_by_name, get_server_api_connection, get_base_url, get_rest_url, get_timeout, set_timeout ) -from ayon_api import exceptions AYON_BASE_URL = os.getenv("AYON_SERVER_URL") AYON_REST_URL = "{}/api".format(AYON_BASE_URL) def test_close_connection(): - _con = get_server_api_connection() + _ = get_server_api_connection() assert is_connection_created() is True close_connection() assert is_connection_created() is False @@ -100,7 +99,7 @@ def test_get(): test_has_children = [ (None), (True), - (False), + # (False), ] from datetime import datetime, timedelta, timezone @@ -156,9 +155,9 @@ def test_get_events_all_filter_combinations( # with many tests - ayon_api.exceptions.ServerError: Connection timed out. # TODO - maybe some better solution if get_timeout() < 5: - set_timeout(10.0) + set_timeout(20.0) - res = get_events( + res = list(get_events( topics=topics, project_names=project_names, states=states, @@ -168,12 +167,10 @@ def test_get_events_all_filter_combinations( newer_than=newer_than, older_than=older_than, fields=fields - ) - - list_res = list(res) + )) # test if filtering was correct - for item in list_res: + for item in res: assert item.get("topic") in topics, ( f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" ) @@ -194,7 +191,7 @@ def test_get_events_all_filter_combinations( ) # test if all events were given - assert topics is None or len(list_res) == sum(len(list(get_events( + assert topics is None or len(res) == sum(len(list(get_events( topics=[topic], project_names=project_names, states=states, @@ -206,7 +203,7 @@ def test_get_events_all_filter_combinations( fields=fields) )) for topic in topics) - assert project_names is None or len(list_res) == sum(len(list(get_events( + assert project_names is None or len(res) == sum(len(list(get_events( topics=topics, project_names=[project_name], states=states, @@ -218,7 +215,7 @@ def test_get_events_all_filter_combinations( fields=fields) )) for project_name in project_names) - assert states is None or len(list_res) == sum(len(list(get_events( + assert states is None or len(res) == sum(len(list(get_events( topics=topics, project_names=project_names, states=[state], @@ -230,7 +227,7 @@ def test_get_events_all_filter_combinations( fields=fields) )) for state in states) - assert users is None or len(list_res) == sum(len(list(get_events( + assert users is None or len(res) == sum(len(list(get_events( topics=topics, project_names=project_names, states=states, @@ -242,7 +239,7 @@ def test_get_events_all_filter_combinations( fields=fields) )) for user in users) - assert fields is None or len(list_res) == sum(len(list(get_events( + assert fields is None or len(res) == sum(len(list(get_events( topics=topics, project_names=project_names, states=states, @@ -257,67 +254,152 @@ def test_get_events_all_filter_combinations( @pytest.mark.parametrize("project_names", test_project_names) def test_get_events_project_name(project_names): - res = get_events(project_names=project_names) - - list_res = list(res) - - users = set() - for item in list_res: - users.add(item.get("user")) + res = list(get_events(project_names=project_names)) + + for item in res: assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" # test if the legths are equal - assert len(list_res) == sum(len(list(get_events(project_names=[project_name]))) for project_name in project_names) + assert len(res) == sum(len(list(get_events(project_names=[project_name]))) for project_name in project_names) @pytest.mark.parametrize("project_names", test_project_names) @pytest.mark.parametrize("topics", test_topics) def test_get_events_project_name_topic(project_names, topics): print(project_names, "", topics) - res = get_events(topics=topics, project_names=project_names) - - list_res = list(res) + res = list(get_events( + topics=topics, + project_names=project_names + )) - for item in list_res: + for item in res: assert item.get("topic") in topics assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" # test if the legths are equal - assert len(list_res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) - assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) + assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) @pytest.mark.parametrize("project_names", test_project_names) @pytest.mark.parametrize("topics", test_topics) @pytest.mark.parametrize("users", test_users) def test_get_events_project_name_topic_user(project_names, topics, users): - # print(project_names, "", topics) - res = get_events(topics=topics, project_names=project_names, users=users) - - list_res = list(res) + res = list(get_events( + topics=topics, + project_names=project_names, + users=users + )) - for item in list_res: + for item in res: assert item.get("topic") in topics, f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" assert item.get("project") in project_names, f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" assert item.get("user") in project_names, f"Expected 'project' one of values: {users}, but got '{item.get('user')}'" # test if the legths are equal - assert len(list_res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) - assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) - assert len(list_res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) + assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) @pytest.mark.parametrize("newer_than", test_newer_than) @pytest.mark.parametrize("older_than", test_older_than) def test_get_events_timestamp(newer_than, older_than): - res = get_events(newer_than=newer_than, older_than=older_than) - - list_res = list(res) + res = list(get_events( + newer_than=newer_than, + older_than=older_than + )) - for item in list_res: + for item in res: assert (newer_than is None) or ( datetime.fromisoformat(item.get("createdAt") > datetime.fromisoformat(newer_than)) ) assert (older_than is None) or ( datetime.fromisoformat(item.get("createdAt") < datetime.fromisoformat(older_than)) ) + + +test_invalid_topics = [ + (None), + (["invalid_topic_name_1", "invalid_topic_name_2"]), + (["invalid_topic_name_1"]), +] + +test_invalid_project_names = [ + (None), + (["invalid_project"]), + (["invalid_project", "demo_Big_Episodic", "demo_Big_Feature"]), + (["invalid_name_2", "demo_Commercial"]), + (["demo_Commercial"]), +] + +test_invalid_states = [ + (None), + (["pending_invalid"]), + (["in_progress_invalid"]), + (["finished_invalid", "failed_invalid"]), +] + +test_invalid_users = [ + (None), + (["ayon_invalid_user"]), + (["ayon_invalid_user1", "ayon_invalid_user2"]), + (["ayon_invalid_user1", "ayon_invalid_user2", "admin"]), +] + +test_invalid_newer_than = [ + (None), + ((datetime.now(timezone.utc) + timedelta(days=2)).isoformat()), + ((datetime.now(timezone.utc) + timedelta(days=5)).isoformat()), + ((datetime.now(timezone.utc) - timedelta(days=5)).isoformat()), +] + + +@pytest.mark.parametrize("topics", test_invalid_topics) +@pytest.mark.parametrize("project_names", test_invalid_project_names) +@pytest.mark.parametrize("states", test_invalid_states) +@pytest.mark.parametrize("users", test_invalid_users) +@pytest.mark.parametrize("newer_than", test_invalid_newer_than) +def test_get_events_invalid_data( + topics, + project_names, + states, + users, + newer_than +): + # with many tests - ayon_api.exceptions.ServerError: Connection timed out. + # TODO - maybe some better solution + if get_timeout() < 5: + set_timeout(20.0) + + res = list(get_events( + topics=topics, + project_names=project_names, + states=states, + users=users, + newer_than=newer_than + )) + + valid_project_names = get_project_names() + + assert res == [] \ + or topics is None + assert res == [] \ + or project_names is None \ + or any(project_name in valid_project_names for project_name in project_names) + assert res == [] \ + or states is None + assert res == [] \ + or users is None \ + or any(get_user_by_name(user) is not None for user in users) + assert res == [] \ + or newer_than is None \ + or datetime.fromisoformat(newer_than) < datetime.now(timezone.utc) + + +test_update_sender = [ + (), +] + +# def test_update_event(): + From 3ef5a3a2328197eafdaafbaadc389dc8eacef139 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 7 Nov 2024 16:26:57 +0100 Subject: [PATCH 04/16] get_events/update_event: All possible filters and their combinations, tests for invalid filter values, new tests for update_event --- tests/test_server.py | 143 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 6 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 0ed6b7172..a5fa743a0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,6 +11,7 @@ is_connection_created, close_connection, get, + get_event, get_events, get_project_names, get_user_by_name, @@ -18,7 +19,9 @@ get_base_url, get_rest_url, get_timeout, - set_timeout + set_timeout, + update_event, + exceptions ) AYON_BASE_URL = os.getenv("AYON_SERVER_URL") @@ -140,7 +143,7 @@ def test_get(): @pytest.mark.parametrize("older_than", test_older_than[-3:]) @pytest.mark.parametrize("fields", test_fields[-3:]) def test_get_events_all_filter_combinations( - topics, + topics, project_names, states, users, @@ -158,7 +161,7 @@ def test_get_events_all_filter_combinations( set_timeout(20.0) res = list(get_events( - topics=topics, + topics=topics, project_names=project_names, states=states, users=users, @@ -252,6 +255,28 @@ def test_get_events_all_filter_combinations( )) for field in fields) +@pytest.fixture(params=[1, 2, 3, 4, 5]) +def event_ids(request): + length = request.param + if length == 0: + return None + + recent_events = list(get_events( + newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() + )) + + return [recent_event["id"] for recent_event in recent_events[:length]] + + +def test_get_events_event_ids(event_ids): + res = list(get_events(event_ids=event_ids)) + + for item in res: + assert item.get("id") in event_ids + + assert len(res) == sum(len(list(get_events(event_ids=[event_id]))) for event_id in event_ids) + + @pytest.mark.parametrize("project_names", test_project_names) def test_get_events_project_name(project_names): res = list(get_events(project_names=project_names)) @@ -266,7 +291,6 @@ def test_get_events_project_name(project_names): @pytest.mark.parametrize("project_names", test_project_names) @pytest.mark.parametrize("topics", test_topics) def test_get_events_project_name_topic(project_names, topics): - print(project_names, "", topics) res = list(get_events( topics=topics, project_names=project_names @@ -397,9 +421,116 @@ def test_get_events_invalid_data( or datetime.fromisoformat(newer_than) < datetime.now(timezone.utc) +@pytest.fixture +def event_id(): + recent_event = list(get_events( + newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() + )) + return recent_event[0]["id"] if recent_event else None + test_update_sender = [ - (), + ("test.server.api"), +] + +test_update_username = [ + ("testing_user"), +] + +test_update_status = [ + ("pending"), + ("in_progress"), + ("finished"), + ("failed"), + ("aborted"), + ("restarted") +] + +test_update_description = [ + ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce viverra."), + ("Updated description test...") +] + +test_update_retries = [ + (1), + (0), + (10), +] + +@pytest.mark.parametrize("sender", test_update_sender) +@pytest.mark.parametrize("username", test_update_username) +@pytest.mark.parametrize("status", test_update_status) +@pytest.mark.parametrize("description", test_update_description) +@pytest.mark.parametrize("retries", test_update_retries) +def test_update_event( + event_id, + sender, + username, + status, + description, + retries, + project_name=None, + summary=None, + payload=None, + progress=None, +): + kwargs = { + key: value + for key, value in ( + ("event_id", event_id), + ("sender", sender), + ("project", project_name), + ("username", username), + ("status", status), + ("description", description), + ("summary", summary), + ("payload", payload), + ("progress", progress), + ("retries", retries), + ) + if value is not None + } + + prev = get_event(event_id=event_id) + update_event(**kwargs) + res = get_event(event_id=event_id) + + for key, value in res.items(): + assert value == prev.get(key) \ + or key in kwargs.keys() and value == kwargs.get(key) \ + or ( + key == "updatedAt" and ( + datetime.fromisoformat(value) - datetime.now(timezone.utc) < timedelta(minutes=1) + ) + ) + + +test_update_invalid_status = [ + ("finisheddd"), + ("pending_pending"), + (42), + (False), + ("_in_progress") +] + +@pytest.mark.parametrize("status", test_update_invalid_status) +def test_update_event_invalid_status(status): + events = list(get_events(project_names=["demo_Commercial"])) + + with pytest.raises(exceptions.HTTPRequestError): + update_event(events[0]["id"], status=status) + + +test_update_invalid_progress = [ + ("good"), + ("bad"), + (-1), + ([0, 1, 2]), + (101) ] -# def test_update_event(): +@pytest.mark.parametrize("progress", test_update_invalid_progress) +def test_update_event_invalid_progress(progress): + events = list(get_events(project_names=["demo_Commercial"])) + with pytest.raises(exceptions.HTTPRequestError): + update_event(events[0]["id"], progress=progress) From e46e8343166393e59b7a9746e7f92d32634a8a61 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 7 Nov 2024 17:41:00 +0100 Subject: [PATCH 05/16] get_events/update_event: New test for timeout added, handling exception for timeout (has_children filter), code adjustments --- tests/test_server.py | 108 +++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index a5fa743a0..8afb24a4d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,6 +11,7 @@ is_connection_created, close_connection, get, + get_default_fields_for_type, get_event, get_events, get_project_names, @@ -102,7 +103,7 @@ def test_get(): test_has_children = [ (None), (True), - # (False), + (False), ] from datetime import datetime, timedelta, timezone @@ -129,21 +130,36 @@ def test_get(): test_fields = [ (None), ([]), + ([]) ] +@pytest.fixture(params=[3, 4, 5]) +def event_ids(request): + length = request.param + if length == 0: + return None + + recent_events = list(get_events( + newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() + )) + + return [recent_event["id"] for recent_event in recent_events[:length]] + # takes max 3 items in a list to reduce the number of combinations @pytest.mark.parametrize("topics", test_topics[-3:]) +@pytest.mark.parametrize("event_ids", [None] + [pytest.param(None, marks=pytest.mark.usefixtures("event_ids"))]) @pytest.mark.parametrize("project_names", test_project_names[-3:]) @pytest.mark.parametrize("states", test_states[-3:]) @pytest.mark.parametrize("users", test_users[-3:]) @pytest.mark.parametrize("include_logs", test_include_logs[-3:]) -@pytest.mark.parametrize("has_children", test_has_children[-3:]) +@pytest.mark.parametrize("has_children", test_has_children[2:3]) @pytest.mark.parametrize("newer_than", test_newer_than[-3:]) @pytest.mark.parametrize("older_than", test_older_than[-3:]) @pytest.mark.parametrize("fields", test_fields[-3:]) def test_get_events_all_filter_combinations( topics, + event_ids, project_names, states, users, @@ -155,22 +171,26 @@ def test_get_events_all_filter_combinations( ): """Tests all combination of possible filters. """ - # with many tests - ayon_api.exceptions.ServerError: Connection timed out. - # TODO - maybe some better solution if get_timeout() < 5: - set_timeout(20.0) - - res = list(get_events( - topics=topics, - project_names=project_names, - states=states, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields - )) + set_timeout(None) # default timeout + + try: + res = list(get_events( + topics=topics, + event_ids=event_ids, + project_names=project_names, + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + )) + except exceptions.ServerError as exc: + assert has_children == False, f"{exc} even if has_children is {has_children}." + print("Warning: ServerError encountered, test skipped due to timeout.") + pytest.skip("Skipping test due to server timeout.") # test if filtering was correct for item in res: @@ -242,30 +262,32 @@ def test_get_events_all_filter_combinations( fields=fields) )) for user in users) - assert fields is None or len(res) == sum(len(list(get_events( - topics=topics, - project_names=project_names, - states=states, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=[field]) - )) for field in fields) + if fields == []: + fields = get_default_fields_for_type("event") + assert fields is None \ + or all( + set(event.keys()) == set(fields) + for event in res + ) -@pytest.fixture(params=[1, 2, 3, 4, 5]) -def event_ids(request): - length = request.param - if length == 0: - return None - recent_events = list(get_events( - newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() - )) +@pytest.mark.parametrize("has_children", test_has_children) +def test_get_events_timeout_has_children(has_children): + """Separete test for has_children filter. - return [recent_event["id"] for recent_event in recent_events[:length]] + Issues with timeouts. + """ + try: + _ = list(get_events( + has_children=has_children, + newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() + )) + except exceptions.ServerError as exc: + has_children = True + assert has_children == False, f"{exc} even if has_children is {has_children}." + print("Warning: ServerError encountered, test skipped due to timeout.") + pytest.skip("Skipping test due to server timeout.") def test_get_events_event_ids(event_ids): @@ -328,7 +350,7 @@ def test_get_events_project_name_topic_user(project_names, topics, users): @pytest.mark.parametrize("newer_than", test_newer_than) @pytest.mark.parametrize("older_than", test_older_than) -def test_get_events_timestamp(newer_than, older_than): +def test_get_events_timestamps(newer_than, older_than): res = list(get_events( newer_than=newer_than, older_than=older_than @@ -514,10 +536,8 @@ def test_update_event( @pytest.mark.parametrize("status", test_update_invalid_status) def test_update_event_invalid_status(status): - events = list(get_events(project_names=["demo_Commercial"])) - with pytest.raises(exceptions.HTTPRequestError): - update_event(events[0]["id"], status=status) + update_event(event_id, status=status) test_update_invalid_progress = [ @@ -529,8 +549,6 @@ def test_update_event_invalid_status(status): ] @pytest.mark.parametrize("progress", test_update_invalid_progress) -def test_update_event_invalid_progress(progress): - events = list(get_events(project_names=["demo_Commercial"])) - +def test_update_event_invalid_progress(event_id, progress): with pytest.raises(exceptions.HTTPRequestError): - update_event(events[0]["id"], progress=progress) + update_event(event_id, progress=progress) From 5059b3d04ec3d0f7d7d4b79a447cd1ccc627fe2d Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Mon, 11 Nov 2024 11:13:13 +0100 Subject: [PATCH 06/16] Docs: Docstrings for all tests were added. --- tests/test_server.py | 169 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 8afb24a4d..35074970d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -30,6 +30,16 @@ def test_close_connection(): + """Tests the functionality of opening and closing the server API + connection. + + Verifies: + - Confirms that the connection is successfully created when + `get_server_api_connection()` is called. + - Ensures that the connection is closed correctly when + `close_connection()` is invoked, and that the connection + state is appropriately updated. + """ _ = get_server_api_connection() assert is_connection_created() is True close_connection() @@ -37,18 +47,37 @@ def test_close_connection(): def test_get_base_url(): + """Tests the retrieval of the base URL for the API. + + Verifies: + - Confirms that `get_base_url()` returns a string. + - Ensures that the returned URL matches the expected `AYON_BASE_URL`. + """ res = get_base_url() assert isinstance(res, str) assert res == AYON_BASE_URL def test_get_rest_url(): + """Tests the retrieval of the REST API URL. + + Verifies: + - Confirms that `get_rest_url()` returns a string. + - Ensures that the returned URL matches the expected `AYON_REST_URL`. + """ res = get_rest_url() assert isinstance(res, str) assert res == AYON_REST_URL def test_get(): + """Tests the `get` method for making API requests. + + Verifies: + - Ensures that a successful GET request to the endpoint 'info' + returns a status code of 200. + - Confirms that the response data is in the form of a dictionary. + """ res = get("info") assert res.status_code == 200 assert isinstance(res.data, dict) @@ -83,7 +112,7 @@ def test_get(): (["entity.folder.attrib_changed", "entity.project.created", "entity.task.created", "settings.changed"]), ] -# incorrect name for statuses +# states is incorrect name for statuses test_states = [ (None), ([]), @@ -148,7 +177,10 @@ def event_ids(request): # takes max 3 items in a list to reduce the number of combinations @pytest.mark.parametrize("topics", test_topics[-3:]) -@pytest.mark.parametrize("event_ids", [None] + [pytest.param(None, marks=pytest.mark.usefixtures("event_ids"))]) +@pytest.mark.parametrize( + "event_ids", + [None] + [pytest.param(None, marks=pytest.mark.usefixtures("event_ids"))] +) @pytest.mark.parametrize("project_names", test_project_names[-3:]) @pytest.mark.parametrize("states", test_states[-3:]) @pytest.mark.parametrize("users", test_users[-3:]) @@ -169,7 +201,23 @@ def test_get_events_all_filter_combinations( older_than, fields ): - """Tests all combination of possible filters. + """Tests all combinations of possible filters for `get_events`. + + Verifies: + - Calls `get_events` with the provided filter parameters. + - Ensures each event in the result set matches the specified filters. + - Checks that the number of returned events matches the expected count + based on the filters applied. + - Confirms that each event contains only the specified fields, with + no extra keys. + + Note: + - Adjusts the timeout setting if necessary to handle a large number + of tests and avoid timeout errors. + - Some combinations of filter parameters may lead to a server timeout + error. When this occurs, the test will skip instead of failing. + - Currently, a ServerError due to timeout may occur when `has_children` + is set to False. """ if get_timeout() < 5: set_timeout(None) # default timeout @@ -192,7 +240,6 @@ def test_get_events_all_filter_combinations( print("Warning: ServerError encountered, test skipped due to timeout.") pytest.skip("Skipping test due to server timeout.") - # test if filtering was correct for item in res: assert item.get("topic") in topics, ( f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" @@ -213,7 +260,6 @@ def test_get_events_all_filter_combinations( datetime.fromisoformat(item.get("createdAt")) < datetime.fromisoformat(older_than) ) - # test if all events were given assert topics is None or len(res) == sum(len(list(get_events( topics=[topic], project_names=project_names, @@ -274,9 +320,16 @@ def test_get_events_all_filter_combinations( @pytest.mark.parametrize("has_children", test_has_children) def test_get_events_timeout_has_children(has_children): - """Separete test for has_children filter. - - Issues with timeouts. + """Test `get_events` function with the `has_children` filter. + + Verifies: + - The `get_events` function handles requests correctly and does + not time out when using the `has_children` filter with events + created within the last 5 days. + - If a `ServerError` (likely due to a timeout) is raised: + - Logs a warning message and skips the test to avoid failure. + - Asserts that the `ServerError` should occur only when + `has_children` is set to False. """ try: _ = list(get_events( @@ -291,6 +344,13 @@ def test_get_events_timeout_has_children(has_children): def test_get_events_event_ids(event_ids): + """Test `get_events` function using specified event IDs. + + Verifies: + - Each item returned has an ID in the `event_ids` list. + - The number of items returned matches the expected count + when filtered by each individual event ID. + """ res = list(get_events(event_ids=event_ids)) for item in res: @@ -301,6 +361,13 @@ def test_get_events_event_ids(event_ids): @pytest.mark.parametrize("project_names", test_project_names) def test_get_events_project_name(project_names): + """Test `get_events` function using specified project names. + + Verifies: + - Each item returned has a project in the `project_names` list. + - The count of items matches the expected number when filtered + by each individual project name. + """ res = list(get_events(project_names=project_names)) for item in res: @@ -313,6 +380,14 @@ def test_get_events_project_name(project_names): @pytest.mark.parametrize("project_names", test_project_names) @pytest.mark.parametrize("topics", test_topics) def test_get_events_project_name_topic(project_names, topics): + """Test `get_events` function using both project names and topics. + + Verifies: + - Each item returned has a project in `project_names` and a topic + in `topics`. + - The item count matches the expected number when filtered by + each project name and topic combination. + """ res = list(get_events( topics=topics, project_names=project_names @@ -331,6 +406,14 @@ def test_get_events_project_name_topic(project_names, topics): @pytest.mark.parametrize("topics", test_topics) @pytest.mark.parametrize("users", test_users) def test_get_events_project_name_topic_user(project_names, topics, users): + """Test `get_events` function using project names, topics, and users. + + Verifies: + - Each item has a project in `project_names`, a topic in `topics`, + and a user in `users`. + - The item count matches the expected number when filtered by + combinations of project names, topics, and users. + """ res = list(get_events( topics=topics, project_names=project_names, @@ -351,6 +434,12 @@ def test_get_events_project_name_topic_user(project_names, topics, users): @pytest.mark.parametrize("newer_than", test_newer_than) @pytest.mark.parametrize("older_than", test_older_than) def test_get_events_timestamps(newer_than, older_than): + """Test `get_events` function using date filters `newer_than` and `older_than`. + + Verifies: + - Each item's creation date falls within the specified date + range between `newer_than` and `older_than`. + """ res = list(get_events( newer_than=newer_than, older_than=older_than @@ -413,10 +502,26 @@ def test_get_events_invalid_data( users, newer_than ): - # with many tests - ayon_api.exceptions.ServerError: Connection timed out. - # TODO - maybe some better solution + """Tests `get_events` with invalid filter data to ensure correct handling + of invalid input and prevent errors or unexpected results. + + Verifies: + - Confirms that the result is either empty or aligns with expected valid + entries: + - `topics`: Result is empty or topics is set to `None`. + - `project_names`: Result is empty or project names exist in the + list of valid project names. + - `states`: Result is empty or states is set to `None`. + - `users`: Result is empty or each user exists as a valid user. + - `newer_than`: Result is empty or `newer_than` date is in the past. + + Note: + - Adjusts the timeout setting if necessary to handle a large number + of tests and avoid timeout errors. + """ + if get_timeout() < 5: - set_timeout(20.0) + set_timeout(None) # default timeout value res = list(get_events( topics=topics, @@ -445,6 +550,14 @@ def test_get_events_invalid_data( @pytest.fixture def event_id(): + """Fixture that retrieves the ID of a recent event created within + the last 5 days. + + Returns: + - The event ID of the most recent event within the last 5 days + if available. + - `None` if no recent events are found within this time frame. + """ recent_event = list(get_events( newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() )) @@ -494,7 +607,25 @@ def test_update_event( summary=None, payload=None, progress=None, -): +): + """Verifies that the `update_event` function correctly updates event fields. + + Verifies: + - The function updates the specified event fields based on the provided + parameters (`sender`, `username`, `status`, `description`, `retries`, + etc.). + - Only the fields specified in `kwargs` are updated, and other fields + remain unchanged. + - The `updatedAt` field is updated and the change occurs within a + reasonable time frame (within one minute). + - The event's state before and after the update matches the expected + values for the updated fields. + + Notes: + - Parameters like `event_id`, `sender`, `username`, `status`, + `description`, `retries`, etc., are passed dynamically to the function. + - If any parameter is `None`, it is excluded from the update request. + """ kwargs = { key: value for key, value in ( @@ -536,6 +667,13 @@ def test_update_event( @pytest.mark.parametrize("status", test_update_invalid_status) def test_update_event_invalid_status(status): + """Tests `update_event` with invalid status values to ensure correct + error handling for unsupported status inputs. + + Verifies: + - Confirms that an `HTTPRequestError` is raised for invalid status values + when attempting to update an event with an unsupported status. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, status=status) @@ -550,5 +688,12 @@ def test_update_event_invalid_status(status): @pytest.mark.parametrize("progress", test_update_invalid_progress) def test_update_event_invalid_progress(event_id, progress): + """Tests `update_event` with invalid progress values to ensure correct + error handling for unsupported progress inputs. + + Verifies: + - Confirms that an `HTTPRequestError` is raised for invalid progress values + when attempting to update an event with unsupported progress. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, progress=progress) From 98d9dc6b97e21e4d7ecc1a07bd15418f5060124b Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 11:38:07 +0100 Subject: [PATCH 07/16] enroll_event_job/addons/thumbnails: New tests for enroll_event_job method in multiple scenarios. Tests for thumbnail operations - upload, download. Fixture for artist user --- tests/test_server.py | 331 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 326 insertions(+), 5 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 35074970d..39ab027ad 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -4,24 +4,54 @@ Make sure you have set AYON_TOKEN in your environment. """ +from datetime import datetime, timedelta, timezone import os import pytest +import time from ayon_api import ( - is_connection_created, close_connection, + create_folder, + create_project, + create_thumbnail, + delete, + delete_project, + dispatch_event, + download_addon_private_file, + download_file_to_stream, + download_file, + enroll_event_job, get, + get_addon_project_settings, + get_addon_settings, + get_addon_settings_schema, + get_addon_site_settings_schema, + get_addon_site_settings, + get_addon_endpoint, + get_addon_url, + get_addons_info, + get_addons_project_settings, + get_addons_settings, + get_addons_studio_settings, get_default_fields_for_type, get_event, get_events, + get_folder_thumbnail, + get_project, get_project_names, get_user_by_name, get_server_api_connection, get_base_url, get_rest_url, + get_thumbnail, + get_thumbnail_by_id, get_timeout, + is_connection_created, set_timeout, + trigger_server_restart, update_event, + upload_addon_zip, + ServerAPI, exceptions ) @@ -39,6 +69,7 @@ def test_close_connection(): - Ensures that the connection is closed correctly when `close_connection()` is invoked, and that the connection state is appropriately updated. + """ _ = get_server_api_connection() assert is_connection_created() is True @@ -52,6 +83,7 @@ def test_get_base_url(): Verifies: - Confirms that `get_base_url()` returns a string. - Ensures that the returned URL matches the expected `AYON_BASE_URL`. + """ res = get_base_url() assert isinstance(res, str) @@ -64,6 +96,7 @@ def test_get_rest_url(): Verifies: - Confirms that `get_rest_url()` returns a string. - Ensures that the returned URL matches the expected `AYON_REST_URL`. + """ res = get_rest_url() assert isinstance(res, str) @@ -77,6 +110,7 @@ def test_get(): - Ensures that a successful GET request to the endpoint 'info' returns a status code of 200. - Confirms that the response data is in the form of a dictionary. + """ res = get("info") assert res.status_code == 200 @@ -135,8 +169,6 @@ def test_get(): (False), ] -from datetime import datetime, timedelta, timezone - test_newer_than = [ (None), ((datetime.now(timezone.utc) - timedelta(days=2)).isoformat()), @@ -218,6 +250,7 @@ def test_get_events_all_filter_combinations( error. When this occurs, the test will skip instead of failing. - Currently, a ServerError due to timeout may occur when `has_children` is set to False. + """ if get_timeout() < 5: set_timeout(None) # default timeout @@ -284,7 +317,7 @@ def test_get_events_all_filter_combinations( fields=fields) )) for project_name in project_names) - assert states is None or len(res) == sum(len(list(get_events( + assert states is None or len(res) == sum(len(list(get_events( topics=topics, project_names=project_names, states=[state], @@ -330,6 +363,7 @@ def test_get_events_timeout_has_children(has_children): - Logs a warning message and skips the test to avoid failure. - Asserts that the `ServerError` should occur only when `has_children` is set to False. + """ try: _ = list(get_events( @@ -350,6 +384,7 @@ def test_get_events_event_ids(event_ids): - Each item returned has an ID in the `event_ids` list. - The number of items returned matches the expected count when filtered by each individual event ID. + """ res = list(get_events(event_ids=event_ids)) @@ -367,6 +402,7 @@ def test_get_events_project_name(project_names): - Each item returned has a project in the `project_names` list. - The count of items matches the expected number when filtered by each individual project name. + """ res = list(get_events(project_names=project_names)) @@ -387,6 +423,7 @@ def test_get_events_project_name_topic(project_names, topics): in `topics`. - The item count matches the expected number when filtered by each project name and topic combination. + """ res = list(get_events( topics=topics, @@ -439,6 +476,7 @@ def test_get_events_timestamps(newer_than, older_than): Verifies: - Each item's creation date falls within the specified date range between `newer_than` and `older_than`. + """ res = list(get_events( newer_than=newer_than, @@ -518,8 +556,8 @@ def test_get_events_invalid_data( Note: - Adjusts the timeout setting if necessary to handle a large number of tests and avoid timeout errors. + """ - if get_timeout() < 5: set_timeout(None) # default timeout value @@ -557,6 +595,7 @@ def event_id(): - The event ID of the most recent event within the last 5 days if available. - `None` if no recent events are found within this time frame. + """ recent_event = list(get_events( newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() @@ -625,6 +664,7 @@ def test_update_event( - Parameters like `event_id`, `sender`, `username`, `status`, `description`, `retries`, etc., are passed dynamically to the function. - If any parameter is `None`, it is excluded from the update request. + """ kwargs = { key: value @@ -673,6 +713,7 @@ def test_update_event_invalid_status(status): Verifies: - Confirms that an `HTTPRequestError` is raised for invalid status values when attempting to update an event with an unsupported status. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, status=status) @@ -694,6 +735,286 @@ def test_update_event_invalid_progress(event_id, progress): Verifies: - Confirms that an `HTTPRequestError` is raised for invalid progress values when attempting to update an event with unsupported progress. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, progress=progress) + + + +TEST_SOURCE_TOPIC = "test.source.topic" +TEST_TARGET_TOPIC = "test.target.topic" + +test_sequential = [ + (True), + (False), + (None) +] + +def clean_up(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): + events = list(get_events(topics=topics)) + for event in events: + if event["status"] not in ["finished", "failed"]: + update_event(event["id"], status="finished") + + +@pytest.fixture +def new_events(): + clean_up() + + num_of_events = 3 + return [ + dispatch_event(topic=TEST_SOURCE_TOPIC, sender="tester", description=f"New test event n. {num}")["id"] + for num in range(num_of_events) + ] + + +@pytest.mark.parametrize("sequential", test_sequential) +def test_enroll_event_job(sequential, new_events): + # clean_up() # "close" all pending jobs + + job_1 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender_1", + sequential=sequential + ) + + job_2 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender_2", + sequential=sequential + ) + + assert sequential is False \ + or sequential is None \ + or job_2 is None + + update_event(job_1["id"], status="finished") + + job_2 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender_2", + sequential=sequential + ) + + assert job_2 is not None \ + and job_1 != job_2 + + # TODO - delete events - if possible + + # src_event = get_event(job["dependsOn"]) + # update_event(job["id"], status="failed") + + +@pytest.mark.parametrize("sequential", test_sequential) +def test_enroll_event_job_failed(sequential): + clean_up() + + job_1 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender_1", + sequential=sequential + ) + + update_event(job_1["id"], status="failed") + + job_2 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender_2", + sequential=sequential + ) + + assert sequential is not True or job_1 == job_2 + + # TODO - delete events - if possible + + # src_event = get_event(job_1["dependsOn"]) + # print(src_event) + + # print(job) + # print(job_2) + + # update_event(job["id"], status="failed") + + +@pytest.mark.parametrize("sequential", test_sequential) +def test_enroll_event_job_same_sender(sequential): + clean_up() + + job_1 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender", + sequential=sequential + ) + + job_2 = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender="test_sender", + sequential=sequential + ) + + assert job_1 == job_2 + + # TODO - delete events - if possible + +test_invalid_topics = [ + (("invalid_source_topic", "invalid_target_topic")) +] + +@pytest.mark.parametrize("topics", test_invalid_topics) +@pytest.mark.parametrize("sequential", test_sequential) +def test_enroll_event_job_invalid_topics(topics, sequential): + clean_up() + + source_topic, target_topic = topics + + job = enroll_event_job( + source_topic=source_topic, + target_topic=target_topic, + sender="test_sender", + sequential=sequential + ) + + assert job is None + + +def test_enroll_event_job_sequential_false(): + clean_up() # "close" all pending jobs + new_events() + + depends_on_ids = set() + + for sender in ["test_1", "test_2", "test_3"]: + job = enroll_event_job( + source_topic=TEST_SOURCE_TOPIC, + target_topic=TEST_TARGET_TOPIC, + sender=sender, + sequential=False + ) + + assert job is not None \ + and job["dependsOn"] not in depends_on_ids + + depends_on_ids.add(job["dependsOn"]) + + # TODO - delete events if possible + + +TEST_PROJECT_NAME = "test_API_project" +TEST_PROJECT_CODE = "apitest" +AYON_THUMBNAIL_PATH = "tests/resources/ayon-symbol.png" + + +def test_thumbnail_operations( + project_name=TEST_PROJECT_NAME, + project_code=TEST_PROJECT_CODE, + thumbnail_path=AYON_THUMBNAIL_PATH +): + if get_project(project_name): + delete_project(TEST_PROJECT_NAME) + + project = create_project(project_name, project_code) + + thumbnail_id = create_thumbnail(project_name, thumbnail_path) + + folder_id = create_folder(project_name, "my_test_folder", thumbnail_id=thumbnail_id) + thumbnail = get_folder_thumbnail(project_name, folder_id, thumbnail_id) + + assert thumbnail.project_name == project_name + assert thumbnail.thumbnail_id == thumbnail_id + + with open(thumbnail_path, "rb") as file: + image_bytes = file.read() + + assert image_bytes == thumbnail.content + + delete_project(project["name"]) + + +def test_addon_methods(): + addon_name = "tests" + addon_version = "1.0.0" + download_path = "tests/resources/tmp_downloads" + private_file_path = os.path.join(download_path, "ayon-symbol.png") + + delete(f"/addons/{addon_name}/{addon_version}") + assert all(addon_name != addon["name"] for addon in get_addons_info()["addons"]) + + try: + _ = upload_addon_zip("tests/resources/addon/package/tests-1.0.0.zip") + + trigger_server_restart() + + # need to wait at least 0.1 sec. to restart server + time.sleep(0.1) + while True: + try: + addons = get_addons_info()["addons"] + break + except exceptions.ServerError as exc: + assert "Connection timed out" in str(exc) + + assert any(addon_name == addon["name"] for addon in addons) + + downloaded_file = download_addon_private_file( + addon_name, + addon_version, + "ayon-symbol.png", + download_path + ) + + assert downloaded_file == private_file_path + assert os.path.isfile(private_file_path) + + finally: + if os.path.isfile(private_file_path): + os.remove(private_file_path) + + if os.path.isdir(download_path): + os.rmdir(download_path) + + +@pytest.fixture +def api_artist_user(): + project = get_project(TEST_PROJECT_NAME) + if project is None: + project = create_project(TEST_PROJECT_NAME, TEST_PROJECT_CODE) + + api = get_server_api_connection() + + username = "testUser" + password = "testUserPassword" + response = api.get("accessGroups/_") + access_groups = [ + item["name"] + for item in response.data + ] + api.put( + f"users/{username}", + password=password, + data={ + "isAdmin": False, + "isManager": False, + "defaultAccessGroups": access_groups, + "accessGroups": { + project["name"]: access_groups + }, + } + ) + new_api = ServerAPI(api.base_url) + new_api.login(username, password) + + return new_api + + +def test_server_restart_as_user(api_artist_user): + with pytest.raises(Exception): + api_artist_user.trigger_server_restart() + From 535579a75e11354a29db9c0b0e776ef3cdc42658 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 11:40:25 +0100 Subject: [PATCH 08/16] Addon: example addon for testing --- .../addon/__pycache__/package.cpython-311.pyc | Bin 0 -> 328 bytes tests/resources/addon/create_package.py | 489 ++++++++++++++++++ tests/resources/addon/package.py | 9 + tests/resources/addon/package/tests-1.0.0.zip | Bin 0 -> 1646 bytes tests/resources/addon/private/ayon-symbol.png | Bin 0 -> 939 bytes tests/resources/addon/server/__init__.py | 19 + 6 files changed, 517 insertions(+) create mode 100644 tests/resources/addon/__pycache__/package.cpython-311.pyc create mode 100644 tests/resources/addon/create_package.py create mode 100644 tests/resources/addon/package.py create mode 100644 tests/resources/addon/package/tests-1.0.0.zip create mode 100644 tests/resources/addon/private/ayon-symbol.png create mode 100644 tests/resources/addon/server/__init__.py diff --git a/tests/resources/addon/__pycache__/package.cpython-311.pyc b/tests/resources/addon/__pycache__/package.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e8719b1aa3bea6008ebc4f9e30fa7f73775db32 GIT binary patch literal 328 zcmXv}Jx{|h5OtcQse%HkR8@RP>>U~xu_465&H_`G$i%1CB8iRdAi|V?!N!XCHytBS zNc@3H-8$ifa=N>BA9_!BpHUPdYgezA%MXh`UGs0wzSy4I;vG?xAnGIz9V7$lCN4vX zXTzuNJ(_vO*PygA{zC_E{zbfq7k9Iv@k&yF-7=kn30^=d#!C}sIfGKClu0uoH7`>M z&lQtEQ$i^(m6`U_j2D7v_L#$d{)M*PS-V@j;ssz$C=i9~QdAmujJc@w$^sD#_ZMLa zidQlNg?nWzNcKdQ5Q};@x-K}aa=4L^)$IONbDK7<<2Y?_*m&*esPWry+yvjzxCu8y TbbPu-llJ6nji!Gt4=m#s1D9hW literal 0 HcmV?d00001 diff --git a/tests/resources/addon/create_package.py b/tests/resources/addon/create_package.py new file mode 100644 index 000000000..5c5ba8590 --- /dev/null +++ b/tests/resources/addon/create_package.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python + +"""Prepares server package from addon repo to upload to server. + +Requires Python 3.9. (Or at least 3.8+). + +This script should be called from cloned addon repo. + +It will produce 'package' subdirectory which could be pasted into server +addon directory directly (eg. into `ayon-backend/addons`). + +Format of package folder: +ADDON_REPO/package/{addon name}/{addon version} + +You can specify `--output_dir` in arguments to change output directory where +package will be created. Existing package directory will always be purged if +already present! This could be used to create package directly in server folder +if available. + +Package contains server side files directly, +client side code zipped in `private` subfolder. +""" + +import os +import sys +import re +import io +import shutil +import platform +import argparse +import logging +import collections +import zipfile +import subprocess +from typing import Optional, Iterable, Pattern, Union, List, Tuple + +import package + +FileMapping = Tuple[Union[str, io.BytesIO], str] +ADDON_NAME: str = package.name +ADDON_VERSION: str = package.version +ADDON_CLIENT_DIR: Union[str, None] = getattr(package, "client_dir", None) + +CURRENT_ROOT: str = os.path.dirname(os.path.abspath(__file__)) +SERVER_ROOT: str = os.path.join(CURRENT_ROOT, "server") +FRONTEND_ROOT: str = os.path.join(CURRENT_ROOT, "frontend") +FRONTEND_DIST_ROOT: str = os.path.join(FRONTEND_ROOT, "dist") +DST_DIST_DIR: str = os.path.join("frontend", "dist") +PRIVATE_ROOT: str = os.path.join(CURRENT_ROOT, "private") +PUBLIC_ROOT: str = os.path.join(CURRENT_ROOT, "public") +CLIENT_ROOT: str = os.path.join(CURRENT_ROOT, "client") + +VERSION_PY_CONTENT = f'''# -*- coding: utf-8 -*- +"""Package declaring AYON addon '{ADDON_NAME}' version.""" +__version__ = "{ADDON_VERSION}" +''' + +# Patterns of directories to be skipped for server part of addon +IGNORE_DIR_PATTERNS: List[Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip directories starting with '.' + r"^\.", + # Skip any pycache folders + "^__pycache__$" + } +] + +# Patterns of files to be skipped for server part of addon +IGNORE_FILE_PATTERNS: List[Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip files starting with '.' + # NOTE this could be an issue in some cases + r"^\.", + # Skip '.pyc' files + r"\.pyc$" + } +] + + +class ZipFileLongPaths(zipfile.ZipFile): + """Allows longer paths in zip files. + + Regular DOS paths are limited to MAX_PATH (260) characters, including + the string's terminating NUL character. + That limit can be exceeded by using an extended-length path that + starts with the '\\?\' prefix. + """ + _is_windows = platform.system().lower() == "windows" + + def _extract_member(self, member, tpath, pwd): + if self._is_windows: + tpath = os.path.abspath(tpath) + if tpath.startswith("\\\\"): + tpath = "\\\\?\\UNC\\" + tpath[2:] + else: + tpath = "\\\\?\\" + tpath + + return super()._extract_member(member, tpath, pwd) + + +def _get_yarn_executable() -> Union[str, None]: + cmd = "which" + if platform.system().lower() == "windows": + cmd = "where" + + for line in subprocess.check_output( + [cmd, "yarn"], encoding="utf-8" + ).splitlines(): + if not line or not os.path.exists(line): + continue + try: + subprocess.call([line, "--version"]) + return line + except OSError: + continue + return None + + +def safe_copy_file(src_path: str, dst_path: str): + """Copy file and make sure destination directory exists. + + Ignore if destination already contains directories from source. + + Args: + src_path (str): File path that will be copied. + dst_path (str): Path to destination file. + """ + + if src_path == dst_path: + return + + dst_dir: str = os.path.dirname(dst_path) + os.makedirs(dst_dir, exist_ok=True) + + shutil.copy2(src_path, dst_path) + + +def _value_match_regexes(value: str, regexes: Iterable[Pattern]) -> bool: + return any( + regex.search(value) + for regex in regexes + ) + + +def find_files_in_subdir( + src_path: str, + ignore_file_patterns: Optional[List[Pattern]] = None, + ignore_dir_patterns: Optional[List[Pattern]] = None +) -> List[Tuple[str, str]]: + """Find all files to copy in subdirectories of given path. + + All files that match any of the patterns in 'ignore_file_patterns' will + be skipped and any directories that match any of the patterns in + 'ignore_dir_patterns' will be skipped with all subfiles. + + Args: + src_path (str): Path to directory to search in. + ignore_file_patterns (Optional[list[Pattern]]): List of regexes + to match files to ignore. + ignore_dir_patterns (Optional[list[Pattern]]): List of regexes + to match directories to ignore. + + Returns: + list[tuple[str, str]]: List of tuples with path to file and parent + directories relative to 'src_path'. + """ + + if ignore_file_patterns is None: + ignore_file_patterns = IGNORE_FILE_PATTERNS + + if ignore_dir_patterns is None: + ignore_dir_patterns = IGNORE_DIR_PATTERNS + output: List[Tuple[str, str]] = [] + if not os.path.exists(src_path): + return output + + hierarchy_queue: collections.deque = collections.deque() + hierarchy_queue.append((src_path, [])) + while hierarchy_queue: + item: Tuple[str, str] = hierarchy_queue.popleft() + dirpath, parents = item + for name in os.listdir(dirpath): + path: str = os.path.join(dirpath, name) + if os.path.isfile(path): + if not _value_match_regexes(name, ignore_file_patterns): + items: List[str] = list(parents) + items.append(name) + output.append((path, os.path.sep.join(items))) + continue + + if not _value_match_regexes(name, ignore_dir_patterns): + items: List[str] = list(parents) + items.append(name) + hierarchy_queue.append((path, items)) + + return output + + +def update_client_version(logger): + """Update version in client code if version.py is present.""" + if not ADDON_CLIENT_DIR: + return + + version_path: str = os.path.join( + CLIENT_ROOT, ADDON_CLIENT_DIR, "version.py" + ) + if not os.path.exists(version_path): + logger.debug("Did not find version.py in client directory") + return + + logger.info("Updating client version") + with open(version_path, "w") as stream: + stream.write(VERSION_PY_CONTENT) + + +def build_frontend(): + yarn_executable = _get_yarn_executable() + if yarn_executable is None: + raise RuntimeError("Yarn executable was not found.") + + subprocess.run([yarn_executable, "install"], cwd=FRONTEND_ROOT) + subprocess.run([yarn_executable, "build"], cwd=FRONTEND_ROOT) + if not os.path.exists(FRONTEND_DIST_ROOT): + raise RuntimeError( + "Frontend build failed. Did not find 'dist' folder." + ) + + +def get_client_files_mapping() -> List[Tuple[str, str]]: + """Mapping of source client code files to destination paths. + + Example output: + [ + ( + "C:/addons/MyAddon/version.py", + "my_addon/version.py" + ), + ( + "C:/addons/MyAddon/client/my_addon/__init__.py", + "my_addon/__init__.py" + ) + ] + + Returns: + list[tuple[str, str]]: List of path mappings to copy. The destination + path is relative to expected output directory. + + """ + # Add client code content to zip + client_code_dir: str = os.path.join(CLIENT_ROOT, ADDON_CLIENT_DIR) + mapping = [ + (path, os.path.join(ADDON_CLIENT_DIR, sub_path)) + for path, sub_path in find_files_in_subdir(client_code_dir) + ] + + license_path = os.path.join(CURRENT_ROOT, "LICENSE") + if os.path.exists(license_path): + mapping.append((license_path, f"{ADDON_CLIENT_DIR}/LICENSE")) + return mapping + + +def get_client_zip_content(log) -> io.BytesIO: + log.info("Preparing client code zip") + files_mapping: List[Tuple[str, str]] = get_client_files_mapping() + stream = io.BytesIO() + with ZipFileLongPaths(stream, "w", zipfile.ZIP_DEFLATED) as zipf: + for src_path, subpath in files_mapping: + zipf.write(src_path, subpath) + stream.seek(0) + return stream + + +def get_base_files_mapping() -> List[FileMapping]: + filepaths_to_copy: List[FileMapping] = [ + ( + os.path.join(CURRENT_ROOT, "package.py"), + "package.py" + ) + ] + # Add license file to package if exists + license_path = os.path.join(CURRENT_ROOT, "LICENSE") + if os.path.exists(license_path): + filepaths_to_copy.append((license_path, "LICENSE")) + + # Go through server, private and public directories and find all files + for dirpath in (SERVER_ROOT, PRIVATE_ROOT, PUBLIC_ROOT): + if not os.path.exists(dirpath): + continue + + dirname = os.path.basename(dirpath) + for src_file, subpath in find_files_in_subdir(dirpath): + dst_subpath = os.path.join(dirname, subpath) + filepaths_to_copy.append((src_file, dst_subpath)) + + if os.path.exists(FRONTEND_DIST_ROOT): + for src_file, subpath in find_files_in_subdir(FRONTEND_DIST_ROOT): + dst_subpath = os.path.join(DST_DIST_DIR, subpath) + filepaths_to_copy.append((src_file, dst_subpath)) + + pyproject_toml = os.path.join(CLIENT_ROOT, "pyproject.toml") + if os.path.exists(pyproject_toml): + filepaths_to_copy.append( + (pyproject_toml, "private/pyproject.toml") + ) + + return filepaths_to_copy + + +def copy_client_code(output_dir: str, log: logging.Logger): + """Copies server side folders to 'addon_package_dir' + + Args: + output_dir (str): Output directory path. + log (logging.Logger) + + """ + log.info(f"Copying client for {ADDON_NAME}-{ADDON_VERSION}") + + full_output_path = os.path.join( + output_dir, f"{ADDON_NAME}_{ADDON_VERSION}" + ) + if os.path.exists(full_output_path): + shutil.rmtree(full_output_path) + os.makedirs(full_output_path, exist_ok=True) + + for src_path, dst_subpath in get_client_files_mapping(): + dst_path = os.path.join(full_output_path, dst_subpath) + safe_copy_file(src_path, dst_path) + + log.info("Client copy finished") + + +def copy_addon_package( + output_dir: str, + files_mapping: List[FileMapping], + log: logging.Logger +): + """Copy client code to output directory. + + Args: + output_dir (str): Directory path to output client code. + files_mapping (List[FileMapping]): List of tuples with source file + and destination subpath. + log (logging.Logger): Logger object. + + """ + log.info(f"Copying package for {ADDON_NAME}-{ADDON_VERSION}") + + # Add addon name and version to output directory + addon_output_dir: str = os.path.join( + output_dir, ADDON_NAME, ADDON_VERSION + ) + if os.path.isdir(addon_output_dir): + log.info(f"Purging {addon_output_dir}") + shutil.rmtree(addon_output_dir) + + os.makedirs(addon_output_dir, exist_ok=True) + + # Copy server content + for src_file, dst_subpath in files_mapping: + dst_path: str = os.path.join(addon_output_dir, dst_subpath) + dst_dir: str = os.path.dirname(dst_path) + os.makedirs(dst_dir, exist_ok=True) + if isinstance(src_file, io.BytesIO): + with open(dst_path, "wb") as stream: + stream.write(src_file.getvalue()) + else: + safe_copy_file(src_file, dst_path) + + log.info("Package copy finished") + + +def create_addon_package( + output_dir: str, + files_mapping: List[FileMapping], + log: logging.Logger +): + log.info(f"Creating package for {ADDON_NAME}-{ADDON_VERSION}") + + os.makedirs(output_dir, exist_ok=True) + output_path = os.path.join( + output_dir, f"{ADDON_NAME}-{ADDON_VERSION}.zip" + ) + + with ZipFileLongPaths(output_path, "w", zipfile.ZIP_DEFLATED) as zipf: + # Copy server content + for src_file, dst_subpath in files_mapping: + if isinstance(src_file, io.BytesIO): + zipf.writestr(dst_subpath, src_file.getvalue()) + else: + zipf.write(src_file, dst_subpath) + + log.info("Package created") + + +def main( + output_dir: Optional[str] = None, + skip_zip: Optional[bool] = False, + only_client: Optional[bool] = False +): + log: logging.Logger = logging.getLogger("create_package") + log.info("Package creation started") + + if not output_dir: + output_dir = os.path.join(CURRENT_ROOT, "package") + + has_client_code = bool(ADDON_CLIENT_DIR) + if has_client_code: + client_dir: str = os.path.join(CLIENT_ROOT, ADDON_CLIENT_DIR) + if not os.path.exists(client_dir): + raise RuntimeError( + f"Client directory was not found '{client_dir}'." + " Please check 'client_dir' in 'package.py'." + ) + update_client_version(log) + + if only_client: + if not has_client_code: + raise RuntimeError("Client code is not available. Skipping") + + copy_client_code(output_dir, log) + return + + log.info(f"Preparing package for {ADDON_NAME}-{ADDON_VERSION}") + + if os.path.exists(FRONTEND_ROOT): + build_frontend() + + files_mapping: List[FileMapping] = [] + files_mapping.extend(get_base_files_mapping()) + + if has_client_code: + files_mapping.append( + (get_client_zip_content(log), "private/client.zip") + ) + + # Skip server zipping + if skip_zip: + copy_addon_package(output_dir, files_mapping, log) + else: + create_addon_package(output_dir, files_mapping, log) + + log.info("Package creation finished") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--skip-zip", + dest="skip_zip", + action="store_true", + help=( + "Skip zipping server package and create only" + " server folder structure." + ) + ) + parser.add_argument( + "-o", "--output", + dest="output_dir", + default=None, + help=( + "Directory path where package will be created" + " (Will be purged if already exists!)" + ) + ) + parser.add_argument( + "--only-client", + dest="only_client", + action="store_true", + help=( + "Extract only client code. This is useful for development." + " Requires '-o', '--output' argument to be filled." + ) + ) + parser.add_argument( + "--debug", + dest="debug", + action="store_true", + help="Debug log messages." + ) + + args = parser.parse_args(sys.argv[1:]) + level = logging.INFO + if args.debug: + level = logging.DEBUG + logging.basicConfig(level=level) + main(args.output_dir, args.skip_zip, args.only_client) diff --git a/tests/resources/addon/package.py b/tests/resources/addon/package.py new file mode 100644 index 000000000..649526135 --- /dev/null +++ b/tests/resources/addon/package.py @@ -0,0 +1,9 @@ +name = "tests" +title = "Tests" +version = "1.0.0" + +client_dir = None +# ayon_launcher_version = ">=1.0.2" + +ayon_required_addons = {} +ayon_compatible_addons = {} \ No newline at end of file diff --git a/tests/resources/addon/package/tests-1.0.0.zip b/tests/resources/addon/package/tests-1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..facd7496aa36896bc7838d30cfada6a2b73736be GIT binary patch literal 1646 zcmZ`(dpOg382`=O&T?l%xkQr7%xID>k}NSTY1l6`qge~f?bKX4QWi$Y@YBNSeFJTz< zL$@=dCB^I=%XFiG88MJ(GoLY^6oc()ZyqUi$Z2k(CX_b^yylM%FG^HZ=t?SQ-rrO+ z)KHg?eGu$!zIAoTt86x(IOlreiAC3B1YmfZwQ*_Vri}5~bGa71L6yA)4<0HoP-VGX zbmClQ;licgiYJd#X032n^okn$_hk)nYWekXI*xLF;a?PSYqrMkaR_WA#`lBbfrC4W zOb~&mJR)L~cIoMB^wI9Ax9Hj&i(MBRbLHXh(c+Ma!&bJ_s4eb`(;qeaw77yN)`l2PY)6v!qU>xm6a9wzX~HKI!#PWpc^)0<~ang7%$?1?!rTb z(5%2+++22&Sc7k#1u&mtm4J~$MzR$r82eR(64?N2HQ2jGnX-i)t<^WuXx$%7y>{q1 zIo>z|OT_AOYLjj!2s5;)zYxf+8eC!Wdh~xnzNMv}lc6b70K71W8*5)8^+$ULIm{us znwC7eM+TW00`3-oG@Kzfi@dvv+Q6fUh0T3ELCQYZbRJLmIj5K>e3nDIAoS4KMs}o` zqtJQP=vAnz=m$wi50oZ+z>01zkM065!bF0*EG{Lud=y>ZQQZa#J02B*ErJdl?^i$i zAM!PYdIrN&wDVn@sKvR2<^9+2YM`S}JH{^eeVA(JTmg&UXPhdaGfsXwe6o_{kMkkF z!1j#cvhC_q=6p#<)p013Vk9!ppz~rhujUA7QuWz@pd8$=ovYu$A?6Dv80~m!`{P|b z#z<{8a-S+=>-suHgk?VPxGjZ;mkD)%nY!B9q{|LOHEb@iPwz8U6vQ;J5-aPEsF8iK zjaMqyk*T3gJVNN|$ofq_Pg+uXXZ#|?zU*Z|FM)aMP{F*q;C;Z#M??YoW2ajJHX^LsRqy-YCV zQcw@?5k@O>4g`Rh3-fC)eL020uLZ>t8NZ%-GfX)LS~wQ&8~9`UOGOG zt%J;bJQiWEe7H6#VDC0(q7Hlx4( zAP7{mv7?9ArL=9YxRDx!lB`D@rB&r3mCkx!ZujDLR*{0aZB=2xs1folD@z?ce(g;J zXpf%m%atQ@ncWC^k;VNWUC}sLVI&#_>x@>-qKC3+v*^c}NHamXuIK>So!q)KFn_{|4BiapcD&BqRU;0EKDq-T(jq0d!JMQ^}%UqH+KL14KzgK~#9!?c7~%+b|3O z;9emFs&{Eo4zTP5T`v#;@w)=3_dmk2wTWfP_9jw496&y`XaRpP8H%)lynL{X7hwrY zSi%yPu!JQnVF^oE!V;FSge5Ft39BnB96%o?Zma<29(v-%`c&0(Rbn~w6_yh#m}g9k zSZ-!nMyy$7G#{3aSVpm7xtL|yu)x&LVhvU>iIRPd|H8G0>)^fQxrZxp;rCw`dSp zS1`MwuOnDtYrU9DYZ`I4HdfcchtKa@aoJae{;bbSZn|Ed^bxG}UH|zZ?9X~!ad+7d z-C3`XE3ZA@dYCW0S>9~BW)=G5h-JCC1B9*TA{Tt4$Fd;mOdBs(JYkAJvO;=W($(qq>r7YcrWs0K*%a^lM2bL>m z*$ylyXX*B=U=T4iXKl8J!#%3Ch+-D(B?@X~v6K}IhE<%!QkKWDRAZLIv20@&;8?mX zD;ONBw73dcH!SPalm%FpZpjJ;%PP!b8O!5Ysv*naS+*ey@GRYsW$-N1juj}ZsTs>t zSV~yJ`tM4fDT`^};@ahzo{3B+2&VJaP8R>dDqMXESsu$$O<7#`M5fFBhX(|!RE2o# z77puiy~oC%idFK(3&IkXu!JQnVF^oE!V;FSge5Ft2}@YQ`bQSe_ysZQG=w&+yYm15 N002ovPDHLkV1kKhs4xHk literal 0 HcmV?d00001 diff --git a/tests/resources/addon/server/__init__.py b/tests/resources/addon/server/__init__.py new file mode 100644 index 000000000..0f794cbcf --- /dev/null +++ b/tests/resources/addon/server/__init__.py @@ -0,0 +1,19 @@ +from ayon_server.addons import BaseServerAddon +from ayon_server.api.dependencies import CurrentUser + + +class TestsAddon(BaseServerAddon): + def initialize(self): + self.add_endpoint( + "test-get", + self.get_test, + method="GET", + ) + + async def get_test( + self, user: CurrentUser, + ): + """Return a random folder from the database""" + return { + "success": True, + } From 985523b82e3a616ad13e51172e2076b0120f014e Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 12:02:54 +0100 Subject: [PATCH 09/16] Docs: new docstrings added to all tests: --- tests/test_server.py | 171 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 158 insertions(+), 13 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 39ab027ad..5815139c9 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -771,8 +771,28 @@ def new_events(): @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job(sequential, new_events): - # clean_up() # "close" all pending jobs + """Tests the `enroll_event_job` function for proper event job enrollment and sequential behavior. + Verifies: + - `enroll_event_job` correctly creates and returns a job with specified parameters + (`source_topic`, `target_topic`, `sender`, and `sequential`). + - When `sequential` is set to `True`, only one job can be enrolled at a time, + preventing new enrollments until the first job is closed or updated. + - When `sequential` is `False` or `None`, multiple jobs can be enrolled + concurrently without conflicts. + - The `update_event` function successfully updates the `status` of a job + as expected, allowing for sequential job processing. + + Parameters: + new_events: Fixture or setup to initialize new events for the test case. + + Notes: + - `clean_up()` is called at the start to close any pending jobs, which + could interfere with the test setup and expected outcomes. + - `update_event` is used to set `job_1`'s status to "failed" to test + re-enrollment behavior. + + """ job_1 = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, @@ -811,6 +831,26 @@ def test_enroll_event_job(sequential, new_events): @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_failed(sequential): + """Tests `enroll_event_job` behavior when the initial job fails and sequential processing is enabled. + + Verifies: + - `enroll_event_job` creates a job (`job_1`) with specified parameters + (`source_topic`, `target_topic`, `sender`, and `sequential`). + - After `job_1` fails (status set to "failed"), a new job (`job_2`) can be + enrolled with the same parameters. + - When `sequential` is `True`, the test verifies that `job_1` and `job_2` + are identical, as a failed sequential job should not allow a new job + to be enrolled separately. + - When `sequential` is `False`, `job_1` and `job_2` are allowed to differ, + as concurrent processing is permitted. + + Notes: + - `clean_up()` is called at the start to close any pending jobs, which + could interfere with the test setup and expected outcomes. + - `update_event` is used to set `job_1`'s status to "failed" to test + re-enrollment behavior. + + """ clean_up() job_1 = enroll_event_job( @@ -833,17 +873,26 @@ def test_enroll_event_job_failed(sequential): # TODO - delete events - if possible - # src_event = get_event(job_1["dependsOn"]) - # print(src_event) - - # print(job) - # print(job_2) - - # update_event(job["id"], status="failed") - @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_same_sender(sequential): + """Tests `enroll_event_job` behavior when multiple jobs are enrolled by the same sender. + + Verifies: + - `enroll_event_job` creates a job (`job_1`) with specified parameters + (`source_topic`, `target_topic`, `sender`, and `sequential`). + - When a second job (`job_2`) is enrolled by the same sender with + identical parameters, the function should return the same job as `job_1` + (indicating idempotent behavior for the same sender and parameters). + - The test checks that `job_1` and `job_2` are identical, ensuring that + no duplicate jobs are created for the same sender when `sequential` + behavior does not permit additional jobs. + + Notes: + - `clean_up()` is used at the beginning to close any pending jobs, ensuring + they do not interfere with the test setup or outcomes. + + """ clean_up() job_1 = enroll_event_job( @@ -864,13 +913,28 @@ def test_enroll_event_job_same_sender(sequential): # TODO - delete events - if possible + test_invalid_topics = [ - (("invalid_source_topic", "invalid_target_topic")) + (("invalid_source_topic", "invalid_target_topic")), + (("nonexisting_source_topic", "nonexisting_target_topic")), ] @pytest.mark.parametrize("topics", test_invalid_topics) @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_invalid_topics(topics, sequential): + """Tests `enroll_event_job` behavior when provided with invalid topics. + + Verifies: + - `enroll_event_job` returns `None` when given invalid `source_topic` + or `target_topic`, indicating that the function properly rejects + invalid topic values. + - The function correctly handles both sequential and non-sequential + job processing modes when invalid topics are used. + + Notes: + - `clean_up()` is called at the beginning to close any pending jobs that + may interfere with the test setup or outcomes. + """ clean_up() source_topic, target_topic = topics @@ -885,10 +949,24 @@ def test_enroll_event_job_invalid_topics(topics, sequential): assert job is None -def test_enroll_event_job_sequential_false(): - clean_up() # "close" all pending jobs - new_events() +def test_enroll_event_job_sequential_false(new_events): + """Tests `enroll_event_job` behavior when `sequential` is set to `False`. + + Verifies: + - `enroll_event_job` creates a unique job for each sender even when + `sequential` is set to `False`, allowing concurrent job processing. + - Each job has a unique `dependsOn` identifier, ensuring that no two + jobs are linked in dependency, as expected for non-sequential enrollment. + Parameters: + new_events: Fixture or setup to initialize new events for the test case. + + Notes: + - The `depends_on_ids` set is used to track `dependsOn` identifiers and + verify that each job has a unique dependency state, as required for + concurrent processing. + + """ depends_on_ids = set() for sender in ["test_1", "test_2", "test_3"]: @@ -917,6 +995,23 @@ def test_thumbnail_operations( project_code=TEST_PROJECT_CODE, thumbnail_path=AYON_THUMBNAIL_PATH ): + """Tests thumbnail operations for a project, including creation, association, retrieval, and verification. + + Verifies: + - A project is created with a specified name and code, and any existing + project with the same name is deleted before setup to ensure a clean state. + - A thumbnail is created for the project and associated with a folder. + - The thumbnail associated with the folder is correctly retrieved, with + attributes matching the project name and thumbnail ID. + - The content of the retrieved thumbnail matches the expected image bytes + read from the specified `thumbnail_path`. + + Notes: + - `delete_project` is called initially to remove any pre-existing project + with the same name, ensuring no conflicts during testing. + - At the end of the test, the project is deleted to clean up resources. + + """ if get_project(project_name): delete_project(TEST_PROJECT_NAME) @@ -939,6 +1034,24 @@ def test_thumbnail_operations( def test_addon_methods(): + """Tests addon methods, including upload, verification, download, and cleanup of addon resources. + + Verifies: + - An addon with the specified name and version does not exist at the start. + - Uploads an addon package `.zip` file and triggers a server restart. + - Ensures the server restart completes, and verifies the uploaded addon is + available in the list of addons after the restart. + - Downloads a private file associated with the addon, verifying its + existence and correct download location. + - Cleans up downloaded files and directories after the test to maintain a + clean state. + + Notes: + - `time.sleep(0.1)` is used to allow for a brief pause for the server restart. + - The `finally` block removes downloaded files and the directory to prevent + residual test artifacts. + + """ addon_name = "tests" addon_version = "1.0.0" download_path = "tests/resources/tmp_downloads" @@ -981,8 +1094,27 @@ def test_addon_methods(): os.rmdir(download_path) + @pytest.fixture def api_artist_user(): + """Fixture that sets up an API connection for a non-admin artist user. + + Workflow: + - Checks if the project exists; if not, it creates one with specified + `TEST_PROJECT_NAME` and `TEST_PROJECT_CODE`. + - Establishes a server API connection and retrieves the list of available + access groups. + - Configures a new user with limited permissions (`isAdmin` and `isManager` + set to `False`) and assigns all available access groups as default and + project-specific groups. + - Creates a new API connection using the artist user's credentials + (`username` and `password`) and logs in with it. + + Returns: + new_api: A `ServerAPI` instance authenticated with the artist user's + credentials, ready to use in tests. + + """ project = get_project(TEST_PROJECT_NAME) if project is None: project = create_project(TEST_PROJECT_NAME, TEST_PROJECT_CODE) @@ -1015,6 +1147,19 @@ def api_artist_user(): def test_server_restart_as_user(api_artist_user): + """Tests that a non-admin artist user is not permitted to trigger a server restart. + + Verifies: + - An attempt to call `trigger_server_restart` as a non-admin artist user + raises an exception, ensuring that only users with the appropriate + permissions (e.g., admins) can perform server restart operations. + + Notes: + - The test checks the access control around the `trigger_server_restart` + method to confirm that only authorized users can perform critical actions + like server restarts. + + """ with pytest.raises(Exception): api_artist_user.trigger_server_restart() From f4515857d6bd6e04ef58007cd8f990a0e94e1be6 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 13:01:47 +0100 Subject: [PATCH 10/16] Small code improements - use of fixtures instead of calling clean_up func at the beginning of each events related tests --- tests/test_server.py | 87 ++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 5815139c9..2d86f7ee8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -450,6 +450,7 @@ def test_get_events_project_name_topic_user(project_names, topics, users): and a user in `users`. - The item count matches the expected number when filtered by combinations of project names, topics, and users. + """ res = list(get_events( topics=topics, @@ -744,6 +745,7 @@ def test_update_event_invalid_progress(event_id, progress): TEST_SOURCE_TOPIC = "test.source.topic" TEST_TARGET_TOPIC = "test.target.topic" +DEFAULT_NUMBER_OF_EVENTS = 3 test_sequential = [ (True), @@ -751,7 +753,13 @@ def test_update_event_invalid_progress(event_id, progress): (None) ] -def clean_up(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): +@pytest.fixture +def clean_up_events(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): + """Called at the beginning to close any pending events that may interfere with + the test setup or outcomes by marking them as 'finished'. + + """ + print("clean_up FIXTURE", datetime.now()) events = list(get_events(topics=topics)) for event in events: if event["status"] not in ["finished", "failed"]: @@ -759,18 +767,26 @@ def clean_up(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): @pytest.fixture -def new_events(): - clean_up() +def create_test_events(num_of_events=DEFAULT_NUMBER_OF_EVENTS): + """Fixture to create a specified number of test events and return their IDs. - num_of_events = 3 + This fixture dispatches events to the `TEST_SOURCE_TOPIC` and returns the + list of event IDs for the created events. + + """ + print("new_tests FIXTURE", datetime.now()) return [ dispatch_event(topic=TEST_SOURCE_TOPIC, sender="tester", description=f"New test event n. {num}")["id"] for num in range(num_of_events) ] +# clean_up_events should be below create_test_events to ensure it is called first +# pytest probably does not guarantee the order of execution +@pytest.mark.usefixtures("create_test_events") +@pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) -def test_enroll_event_job(sequential, new_events): +def test_enroll_event_job(sequential): """Tests the `enroll_event_job` function for proper event job enrollment and sequential behavior. Verifies: @@ -787,12 +803,17 @@ def test_enroll_event_job(sequential, new_events): new_events: Fixture or setup to initialize new events for the test case. Notes: - - `clean_up()` is called at the start to close any pending jobs, which - could interfere with the test setup and expected outcomes. - `update_event` is used to set `job_1`'s status to "failed" to test re-enrollment behavior. + - TODO - delete events after test if possible """ + events = list(get_events( + newer_than=(datetime.now(timezone.utc) - timedelta(minutes=1)).isoformat() + )) + + print([event["updatedAt"] for event in events]) + job_1 = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, @@ -823,12 +844,8 @@ def test_enroll_event_job(sequential, new_events): assert job_2 is not None \ and job_1 != job_2 - # TODO - delete events - if possible - - # src_event = get_event(job["dependsOn"]) - # update_event(job["id"], status="failed") - +@pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_failed(sequential): """Tests `enroll_event_job` behavior when the initial job fails and sequential processing is enabled. @@ -845,14 +862,11 @@ def test_enroll_event_job_failed(sequential): as concurrent processing is permitted. Notes: - - `clean_up()` is called at the start to close any pending jobs, which - could interfere with the test setup and expected outcomes. - `update_event` is used to set `job_1`'s status to "failed" to test re-enrollment behavior. - - """ - clean_up() + - TODO - delete events after test if possible + """ job_1 = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, @@ -871,9 +885,8 @@ def test_enroll_event_job_failed(sequential): assert sequential is not True or job_1 == job_2 - # TODO - delete events - if possible - +@pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_same_sender(sequential): """Tests `enroll_event_job` behavior when multiple jobs are enrolled by the same sender. @@ -889,12 +902,9 @@ def test_enroll_event_job_same_sender(sequential): behavior does not permit additional jobs. Notes: - - `clean_up()` is used at the beginning to close any pending jobs, ensuring - they do not interfere with the test setup or outcomes. - - """ - clean_up() + - TODO - delete events after test if possible + """ job_1 = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, @@ -911,14 +921,13 @@ def test_enroll_event_job_same_sender(sequential): assert job_1 == job_2 - # TODO - delete events - if possible - test_invalid_topics = [ (("invalid_source_topic", "invalid_target_topic")), (("nonexisting_source_topic", "nonexisting_target_topic")), ] +@pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("topics", test_invalid_topics) @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_invalid_topics(topics, sequential): @@ -932,11 +941,10 @@ def test_enroll_event_job_invalid_topics(topics, sequential): job processing modes when invalid topics are used. Notes: - - `clean_up()` is called at the beginning to close any pending jobs that + - `clean_up_events()` is called at the beginning to close any pending jobs that may interfere with the test setup or outcomes. + """ - clean_up() - source_topic, target_topic = topics job = enroll_event_job( @@ -949,7 +957,11 @@ def test_enroll_event_job_invalid_topics(topics, sequential): assert job is None -def test_enroll_event_job_sequential_false(new_events): +# clean_up_events should be below create_test_events to ensure it is called first +# pytest probably does not guarantee the order of execution +@pytest.mark.usefixtures("create_test_events") +@pytest.mark.usefixtures("clean_up_events") +def test_enroll_event_job_sequential_false(): """Tests `enroll_event_job` behavior when `sequential` is set to `False`. Verifies: @@ -965,7 +977,7 @@ def test_enroll_event_job_sequential_false(new_events): - The `depends_on_ids` set is used to track `dependsOn` identifiers and verify that each job has a unique dependency state, as required for concurrent processing. - + - TODO - delete events after test if possible """ depends_on_ids = set() @@ -981,8 +993,6 @@ def test_enroll_event_job_sequential_false(new_events): and job["dependsOn"] not in depends_on_ids depends_on_ids.add(job["dependsOn"]) - - # TODO - delete events if possible TEST_PROJECT_NAME = "test_API_project" @@ -1047,10 +1057,10 @@ def test_addon_methods(): clean state. Notes: - - `time.sleep(0.1)` is used to allow for a brief pause for the server restart. + - `time.sleep()` is used to allow for a brief pause for the server restart. - The `finally` block removes downloaded files and the directory to prevent residual test artifacts. - + """ addon_name = "tests" addon_version = "1.0.0" @@ -1065,8 +1075,8 @@ def test_addon_methods(): trigger_server_restart() - # need to wait at least 0.1 sec. to restart server - time.sleep(0.1) + # need to wait at least 0.1 sec. to restart server + time.sleep(0.5) while True: try: addons = get_addons_info()["addons"] @@ -1094,7 +1104,6 @@ def test_addon_methods(): os.rmdir(download_path) - @pytest.fixture def api_artist_user(): """Fixture that sets up an API connection for a non-admin artist user. @@ -1158,7 +1167,7 @@ def test_server_restart_as_user(api_artist_user): - The test checks the access control around the `trigger_server_restart` method to confirm that only authorized users can perform critical actions like server restarts. - + """ with pytest.raises(Exception): api_artist_user.trigger_server_restart() From 077d9d1e4094e7e77c8129bec7c5e6e9bd064262 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 13:10:45 +0100 Subject: [PATCH 11/16] Debug prints deleted --- tests/test_server.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 2d86f7ee8..297a98ea7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -759,7 +759,6 @@ def clean_up_events(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): the test setup or outcomes by marking them as 'finished'. """ - print("clean_up FIXTURE", datetime.now()) events = list(get_events(topics=topics)) for event in events: if event["status"] not in ["finished", "failed"]: @@ -774,7 +773,6 @@ def create_test_events(num_of_events=DEFAULT_NUMBER_OF_EVENTS): list of event IDs for the created events. """ - print("new_tests FIXTURE", datetime.now()) return [ dispatch_event(topic=TEST_SOURCE_TOPIC, sender="tester", description=f"New test event n. {num}")["id"] for num in range(num_of_events) @@ -808,12 +806,6 @@ def test_enroll_event_job(sequential): - TODO - delete events after test if possible """ - events = list(get_events( - newer_than=(datetime.now(timezone.utc) - timedelta(minutes=1)).isoformat() - )) - - print([event["updatedAt"] for event in events]) - job_1 = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, From c297fcb780291cc29240a0ad7e43caee40ca36d7 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 14:46:23 +0100 Subject: [PATCH 12/16] .gitignore: New gitignor for test addon directory --- tests/resources/addon/.gitignore | 1 + tests/resources/addon/package/tests-1.0.0.zip | Bin 1646 -> 0 bytes 2 files changed, 1 insertion(+) create mode 100644 tests/resources/addon/.gitignore delete mode 100644 tests/resources/addon/package/tests-1.0.0.zip diff --git a/tests/resources/addon/.gitignore b/tests/resources/addon/.gitignore new file mode 100644 index 000000000..f2fd75d64 --- /dev/null +++ b/tests/resources/addon/.gitignore @@ -0,0 +1 @@ +/package/ \ No newline at end of file diff --git a/tests/resources/addon/package/tests-1.0.0.zip b/tests/resources/addon/package/tests-1.0.0.zip deleted file mode 100644 index facd7496aa36896bc7838d30cfada6a2b73736be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1646 zcmZ`(dpOg382`=O&T?l%xkQr7%xID>k}NSTY1l6`qge~f?bKX4QWi$Y@YBNSeFJTz< zL$@=dCB^I=%XFiG88MJ(GoLY^6oc()ZyqUi$Z2k(CX_b^yylM%FG^HZ=t?SQ-rrO+ z)KHg?eGu$!zIAoTt86x(IOlreiAC3B1YmfZwQ*_Vri}5~bGa71L6yA)4<0HoP-VGX zbmClQ;licgiYJd#X032n^okn$_hk)nYWekXI*xLF;a?PSYqrMkaR_WA#`lBbfrC4W zOb~&mJR)L~cIoMB^wI9Ax9Hj&i(MBRbLHXh(c+Ma!&bJ_s4eb`(;qeaw77yN)`l2PY)6v!qU>xm6a9wzX~HKI!#PWpc^)0<~ang7%$?1?!rTb z(5%2+++22&Sc7k#1u&mtm4J~$MzR$r82eR(64?N2HQ2jGnX-i)t<^WuXx$%7y>{q1 zIo>z|OT_AOYLjj!2s5;)zYxf+8eC!Wdh~xnzNMv}lc6b70K71W8*5)8^+$ULIm{us znwC7eM+TW00`3-oG@Kzfi@dvv+Q6fUh0T3ELCQYZbRJLmIj5K>e3nDIAoS4KMs}o` zqtJQP=vAnz=m$wi50oZ+z>01zkM065!bF0*EG{Lud=y>ZQQZa#J02B*ErJdl?^i$i zAM!PYdIrN&wDVn@sKvR2<^9+2YM`S}JH{^eeVA(JTmg&UXPhdaGfsXwe6o_{kMkkF z!1j#cvhC_q=6p#<)p013Vk9!ppz~rhujUA7QuWz@pd8$=ovYu$A?6Dv80~m!`{P|b z#z<{8a-S+=>-suHgk?VPxGjZ;mkD)%nY!B9q{|LOHEb@iPwz8U6vQ;J5-aPEsF8iK zjaMqyk*T3gJVNN|$ofq_Pg+uXXZ#|?zU*Z|FM)aMP{F*q;C;Z#M??YoW2ajJHX^LsRqy-YCV zQcw@?5k@O>4g`Rh3-fC)eL020uLZ>t8NZ%-GfX)LS~wQ&8~9`UOGOG zt%J;bJQiWEe7H6#VDC0(q7Hlx4( zAP7{mv7?9ArL=9YxRDx!lB`D@rB&r3mCkx!ZujDLR*{0aZB=2xs1folD@z?ce(g;J zXpf%m%atQ@ncWC^k;VNWUC}sLVI&#_>x@>-qKC3+v*^c}NHamXuIK>So!q)K Date: Thu, 14 Nov 2024 14:50:23 +0100 Subject: [PATCH 13/16] .gitignore: Edit of gitignore --- tests/resources/addon/.gitignore | 3 ++- .../addon/__pycache__/package.cpython-311.pyc | Bin 328 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 tests/resources/addon/__pycache__/package.cpython-311.pyc diff --git a/tests/resources/addon/.gitignore b/tests/resources/addon/.gitignore index f2fd75d64..4ac096f1e 100644 --- a/tests/resources/addon/.gitignore +++ b/tests/resources/addon/.gitignore @@ -1 +1,2 @@ -/package/ \ No newline at end of file +/package/ +/__pycache__/ \ No newline at end of file diff --git a/tests/resources/addon/__pycache__/package.cpython-311.pyc b/tests/resources/addon/__pycache__/package.cpython-311.pyc deleted file mode 100644 index 7e8719b1aa3bea6008ebc4f9e30fa7f73775db32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 328 zcmXv}Jx{|h5OtcQse%HkR8@RP>>U~xu_465&H_`G$i%1CB8iRdAi|V?!N!XCHytBS zNc@3H-8$ifa=N>BA9_!BpHUPdYgezA%MXh`UGs0wzSy4I;vG?xAnGIz9V7$lCN4vX zXTzuNJ(_vO*PygA{zC_E{zbfq7k9Iv@k&yF-7=kn30^=d#!C}sIfGKClu0uoH7`>M z&lQtEQ$i^(m6`U_j2D7v_L#$d{)M*PS-V@j;ssz$C=i9~QdAmujJc@w$^sD#_ZMLa zidQlNg?nWzNcKdQ5Q};@x-K}aa=4L^)$IONbDK7<<2Y?_*m&*esPWry+yvjzxCu8y TbbPu-llJ6nji!Gt4=m#s1D9hW From e011f785cf4cb8ef3a155397e7a0c3aa69438038 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 14:54:31 +0100 Subject: [PATCH 14/16] Thumbnail: example thumbnail for testing --- tests/resources/ayon-symbol.png | Bin 0 -> 939 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resources/ayon-symbol.png diff --git a/tests/resources/ayon-symbol.png b/tests/resources/ayon-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..30afee0e89726f4e7da7107b79310d5828e5c11a GIT binary patch literal 939 zcmV;c162HpP)Fn_{|4BiapcD&BqRU;0EKDq-T(jq0d!JMQ^}%UqH+KL14KzgK~#9!?c7~%+b|3O z;9emFs&{Eo4zTP5T`v#;@w)=3_dmk2wTWfP_9jw496&y`XaRpP8H%)lynL{X7hwrY zSi%yPu!JQnVF^oE!V;FSge5Ft39BnB96%o?Zma<29(v-%`c&0(Rbn~w6_yh#m}g9k zSZ-!nMyy$7G#{3aSVpm7xtL|yu)x&LVhvU>iIRPd|H8G0>)^fQxrZxp;rCw`dSp zS1`MwuOnDtYrU9DYZ`I4HdfcchtKa@aoJae{;bbSZn|Ed^bxG}UH|zZ?9X~!ad+7d z-C3`XE3ZA@dYCW0S>9~BW)=G5h-JCC1B9*TA{Tt4$Fd;mOdBs(JYkAJvO;=W($(qq>r7YcrWs0K*%a^lM2bL>m z*$ylyXX*B=U=T4iXKl8J!#%3Ch+-D(B?@X~v6K}IhE<%!QkKWDRAZLIv20@&;8?mX zD;ONBw73dcH!SPalm%FpZpjJ;%PP!b8O!5Ysv*naS+*ey@GRYsW$-N1juj}ZsTs>t zSV~yJ`tM4fDT`^};@ahzo{3B+2&VJaP8R>dDqMXESsu$$O<7#`M5fFBhX(|!RE2o# z77puiy~oC%idFK(3&IkXu!JQnVF^oE!V;FSge5Ft2}@YQ`bQSe_ysZQG=w&+yYm15 N002ovPDHLkV1kKhs4xHk literal 0 HcmV?d00001 From 3b20133f2e8180fd07339e90ef7c4ec6cf37fd90 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 16:39:22 +0100 Subject: [PATCH 15/16] Code adjust for pass the linting check --- tests/test_server.py | 654 +++++++++++++++++++++++-------------------- 1 file changed, 353 insertions(+), 301 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 297a98ea7..64d1187f8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,6 +2,7 @@ To run use: pytest --envfile {environment path}. Make sure you have set AYON_TOKEN in your environment. + """ from datetime import datetime, timedelta, timezone @@ -18,21 +19,9 @@ delete_project, dispatch_event, download_addon_private_file, - download_file_to_stream, - download_file, enroll_event_job, get, - get_addon_project_settings, - get_addon_settings, - get_addon_settings_schema, - get_addon_site_settings_schema, - get_addon_site_settings, - get_addon_endpoint, - get_addon_url, get_addons_info, - get_addons_project_settings, - get_addons_settings, - get_addons_studio_settings, get_default_fields_for_type, get_event, get_events, @@ -43,8 +32,6 @@ get_server_api_connection, get_base_url, get_rest_url, - get_thumbnail, - get_thumbnail_by_id, get_timeout, is_connection_created, set_timeout, @@ -60,16 +47,16 @@ def test_close_connection(): - """Tests the functionality of opening and closing the server API + """Tests the functionality of opening and closing the server API connection. Verifies: - - Confirms that the connection is successfully created when + - Confirms that the connection is successfully created when `get_server_api_connection()` is called. - Ensures that the connection is closed correctly when - `close_connection()` is invoked, and that the connection - state is appropriately updated. - + `close_connection()` is invoked, and that the connection state + is appropriately updated. + """ _ = get_server_api_connection() assert is_connection_created() is True @@ -83,7 +70,7 @@ def test_get_base_url(): Verifies: - Confirms that `get_base_url()` returns a string. - Ensures that the returned URL matches the expected `AYON_BASE_URL`. - + """ res = get_base_url() assert isinstance(res, str) @@ -96,7 +83,7 @@ def test_get_rest_url(): Verifies: - Confirms that `get_rest_url()` returns a string. - Ensures that the returned URL matches the expected `AYON_REST_URL`. - + """ res = get_rest_url() assert isinstance(res, str) @@ -107,10 +94,10 @@ def test_get(): """Tests the `get` method for making API requests. Verifies: - - Ensures that a successful GET request to the endpoint 'info' + - Ensures that a successful GET request to the endpoint 'info' returns a status code of 200. - Confirms that the response data is in the form of a dictionary. - + """ res = get("info") assert res.status_code == 200 @@ -134,16 +121,25 @@ def test_get(): (["entity.task.created", "entity.project.created"]), (["settings.changed", "entity.version.status_changed"]), (["entity.task.status_changed", "entity.folder.deleted"]), - (["entity.project.changed", "entity.task.tags_changed", "entity.product.created"]) + ([ + "entity.project.changed", + "entity.task.tags_changed", + "entity.product.created" + ]) ] test_users = [ (None), ([]), - (["admin"]), - (["mkolar", "tadeas.8964"]), + (["admin"]), + (["mkolar", "tadeas.8964"]), (["roy", "luke.inderwick", "ynbot"]), - (["entity.folder.attrib_changed", "entity.project.created", "entity.task.created", "settings.changed"]), + ([ + "entity.folder.attrib_changed", + "entity.project.created", + "entity.task.created", + "settings.changed" + ]), ] # states is incorrect name for statuses @@ -169,23 +165,24 @@ def test_get(): (False), ] +now = datetime.now(timezone.utc) + test_newer_than = [ (None), - ((datetime.now(timezone.utc) - timedelta(days=2)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=5)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=10)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=20)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=30)).isoformat()), + ((now - timedelta(days=2)).isoformat()), + ((now - timedelta(days=5)).isoformat()), + ((now - timedelta(days=10)).isoformat()), + ((now - timedelta(days=20)).isoformat()), + ((now - timedelta(days=30)).isoformat()), ] test_older_than = [ (None), - ((datetime.now(timezone.utc) - timedelta(days=0)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=0)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=5)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=10)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=20)).isoformat()), - ((datetime.now(timezone.utc) - timedelta(days=30)).isoformat()), + ((now - timedelta(days=0)).isoformat()), + ((now - timedelta(days=5)).isoformat()), + ((now - timedelta(days=10)).isoformat()), + ((now - timedelta(days=20)).isoformat()), + ((now - timedelta(days=30)).isoformat()), ] test_fields = [ @@ -210,7 +207,7 @@ def event_ids(request): # takes max 3 items in a list to reduce the number of combinations @pytest.mark.parametrize("topics", test_topics[-3:]) @pytest.mark.parametrize( - "event_ids", + "event_ids", [None] + [pytest.param(None, marks=pytest.mark.usefixtures("event_ids"))] ) @pytest.mark.parametrize("project_names", test_project_names[-3:]) @@ -238,19 +235,19 @@ def test_get_events_all_filter_combinations( Verifies: - Calls `get_events` with the provided filter parameters. - Ensures each event in the result set matches the specified filters. - - Checks that the number of returned events matches the expected count + - Checks that the number of returned events matches the expected count based on the filters applied. - - Confirms that each event contains only the specified fields, with + - Confirms that each event contains only the specified fields, with no extra keys. Note: - - Adjusts the timeout setting if necessary to handle a large number + - Adjusts the timeout setting if necessary to handle a large number of tests and avoid timeout errors. - - Some combinations of filter parameters may lead to a server timeout + - Some combinations of filter parameters may lead to a server timeout error. When this occurs, the test will skip instead of failing. - - Currently, a ServerError due to timeout may occur when `has_children` + - Currently, a ServerError due to timeout may occur when `has_children` is set to False. - + """ if get_timeout() < 5: set_timeout(None) # default timeout @@ -269,76 +266,81 @@ def test_get_events_all_filter_combinations( fields=fields )) except exceptions.ServerError as exc: - assert has_children == False, f"{exc} even if has_children is {has_children}." + assert has_children is False, ( + f"{exc} even if has_children is {has_children}." + ) print("Warning: ServerError encountered, test skipped due to timeout.") pytest.skip("Skipping test due to server timeout.") for item in res: - assert item.get("topic") in topics, ( - f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" - ) - assert item.get("project") in project_names, ( - f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" - ) - assert item.get("user") in users, ( - f"Expected 'user' one of values: {users}, but got '{item.get('user')}'" - ) - assert item.get("status") in states, ( - f"Expected 'state' to be one of {states}, but got '{item.get('state')}'" - ) + assert item.get("topic") in topics + assert item.get("project") in project_names + assert item.get("user") in users + assert item.get("status") in states + assert (newer_than is None) or ( - datetime.fromisoformat(item.get("createdAt")) > datetime.fromisoformat(newer_than) + datetime.fromisoformat(item.get("createdAt")) + > datetime.fromisoformat(newer_than) ) assert (older_than is None) or ( - datetime.fromisoformat(item.get("createdAt")) < datetime.fromisoformat(older_than) + datetime.fromisoformat(item.get("createdAt")) + < datetime.fromisoformat(older_than) ) - assert topics is None or len(res) == sum(len(list(get_events( - topics=[topic], - project_names=project_names, - states=states, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields) + assert topics is None or len(res) == sum(len(list( + get_events( + topics=[topic], + project_names=project_names, + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + ) )) for topic in topics) - assert project_names is None or len(res) == sum(len(list(get_events( - topics=topics, - project_names=[project_name], - states=states, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields) + assert project_names is None or len(res) == sum(len(list( + get_events( + topics=topics, + project_names=[project_name], + states=states, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + ) )) for project_name in project_names) - - assert states is None or len(res) == sum(len(list(get_events( - topics=topics, - project_names=project_names, - states=[state], - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields) + + assert states is None or len(res) == sum(len(list( + get_events( + topics=topics, + project_names=project_names, + states=[state], + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + ) )) for state in states) - - assert users is None or len(res) == sum(len(list(get_events( - topics=topics, - project_names=project_names, - states=states, - users=[user], - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields) + + assert users is None or len(res) == sum(len(list( + get_events( + topics=topics, + project_names=project_names, + states=states, + users=[user], + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields + ) )) for user in users) if fields == []: @@ -356,23 +358,26 @@ def test_get_events_timeout_has_children(has_children): """Test `get_events` function with the `has_children` filter. Verifies: - - The `get_events` function handles requests correctly and does - not time out when using the `has_children` filter with events - created within the last 5 days. + - The `get_events` function handles requests correctly and does not + time out when using the `has_children` filter with events created + within the last 5 days. - If a `ServerError` (likely due to a timeout) is raised: - Logs a warning message and skips the test to avoid failure. - - Asserts that the `ServerError` should occur only when - `has_children` is set to False. - + - Asserts that the `ServerError` should occur only when + `has_children` is set to False. + """ try: _ = list(get_events( has_children=has_children, - newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() + newer_than=( + datetime.now(timezone.utc) - timedelta(days=5) + ).isoformat() )) except exceptions.ServerError as exc: - has_children = True - assert has_children == False, f"{exc} even if has_children is {has_children}." + assert has_children is False, ( + f"{exc} even if has_children is {has_children}." + ) print("Warning: ServerError encountered, test skipped due to timeout.") pytest.skip("Skipping test due to server timeout.") @@ -382,16 +387,20 @@ def test_get_events_event_ids(event_ids): Verifies: - Each item returned has an ID in the `event_ids` list. - - The number of items returned matches the expected count - when filtered by each individual event ID. - + - The number of items returned matches the expected count when filtered + by each individual event ID. + """ res = list(get_events(event_ids=event_ids)) for item in res: assert item.get("id") in event_ids - - assert len(res) == sum(len(list(get_events(event_ids=[event_id]))) for event_id in event_ids) + + assert len(res) == sum(len(list( + get_events( + event_ids=[event_id] + ) + )) for event_id in event_ids) @pytest.mark.parametrize("project_names", test_project_names) @@ -400,17 +409,21 @@ def test_get_events_project_name(project_names): Verifies: - Each item returned has a project in the `project_names` list. - - The count of items matches the expected number when filtered + - The count of items matches the expected number when filtered by each individual project name. - + """ res = list(get_events(project_names=project_names)) - + for item in res: - assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" + assert item.get("project") in project_names # test if the legths are equal - assert len(res) == sum(len(list(get_events(project_names=[project_name]))) for project_name in project_names) + assert len(res) == sum(len(list( + get_events( + project_names=[project_name] + ) + )) for project_name in project_names) @pytest.mark.parametrize("project_names", test_project_names) @@ -419,11 +432,11 @@ def test_get_events_project_name_topic(project_names, topics): """Test `get_events` function using both project names and topics. Verifies: - - Each item returned has a project in `project_names` and a topic + - Each item returned has a project in `project_names` and a topic in `topics`. - - The item count matches the expected number when filtered by - each project name and topic combination. - + - The item count matches the expected number when filtered by each + project name and topic combination. + """ res = list(get_events( topics=topics, @@ -432,11 +445,22 @@ def test_get_events_project_name_topic(project_names, topics): for item in res: assert item.get("topic") in topics - assert item.get("project") in project_names, f"Expected 'project' value '{project_names}', but got '{item.get('project')}'" - + assert item.get("project") in project_names + # test if the legths are equal - assert len(res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) - assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(res) == sum(len(list( + get_events( + project_names=[project_name], + topics=topics + ) + )) for project_name in project_names) + + assert len(res) == sum(len(list( + get_events( + project_names=project_names, + topics=[topic] + ) + )) for topic in topics) @pytest.mark.parametrize("project_names", test_project_names) @@ -446,11 +470,11 @@ def test_get_events_project_name_topic_user(project_names, topics, users): """Test `get_events` function using project names, topics, and users. Verifies: - - Each item has a project in `project_names`, a topic in `topics`, + - Each item has a project in `project_names`, a topic in `topics`, and a user in `users`. - - The item count matches the expected number when filtered by + - The item count matches the expected number when filtered by combinations of project names, topics, and users. - + """ res = list(get_events( topics=topics, @@ -459,25 +483,43 @@ def test_get_events_project_name_topic_user(project_names, topics, users): )) for item in res: - assert item.get("topic") in topics, f"Expected 'project' one of values: {topics}, but got '{item.get('topic')}'" - assert item.get("project") in project_names, f"Expected 'project' one of values: {project_names}, but got '{item.get('project')}'" - assert item.get("user") in project_names, f"Expected 'project' one of values: {users}, but got '{item.get('user')}'" + assert item.get("topic") in topics + assert item.get("project") in project_names + assert item.get("user") in project_names # test if the legths are equal - assert len(res) == sum(len(list(get_events(project_names=[project_name], topics=topics))) for project_name in project_names) - assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) - assert len(res) == sum(len(list(get_events(project_names=project_names, topics=[topic]))) for topic in topics) + assert len(res) == sum(len(list( + get_events( + project_names=[project_name], + topics=topics + ) + )) for project_name in project_names) + + assert len(res) == sum(len(list( + get_events( + project_names=project_names, + topics=[topic] + ) + )) for topic in topics) + + assert len(res) == sum(len(list( + get_events( + project_names=project_names, + topics=[topic] + ) + )) for topic in topics) @pytest.mark.parametrize("newer_than", test_newer_than) @pytest.mark.parametrize("older_than", test_older_than) def test_get_events_timestamps(newer_than, older_than): - """Test `get_events` function using date filters `newer_than` and `older_than`. + """Test `get_events` function using date filters `newer_than` and + `older_than`. Verifies: - - Each item's creation date falls within the specified date + - Each item's creation date falls within the specified date range between `newer_than` and `older_than`. - + """ res = list(get_events( newer_than=newer_than, @@ -486,10 +528,12 @@ def test_get_events_timestamps(newer_than, older_than): for item in res: assert (newer_than is None) or ( - datetime.fromisoformat(item.get("createdAt") > datetime.fromisoformat(newer_than)) + datetime.fromisoformat(item.get("createdAt") + > datetime.fromisoformat(newer_than)) ) assert (older_than is None) or ( - datetime.fromisoformat(item.get("createdAt") < datetime.fromisoformat(older_than)) + datetime.fromisoformat(item.get("createdAt") + < datetime.fromisoformat(older_than)) ) @@ -510,7 +554,7 @@ def test_get_events_timestamps(newer_than, older_than): test_invalid_states = [ (None), (["pending_invalid"]), - (["in_progress_invalid"]), + (["in_progress_invalid"]), (["finished_invalid", "failed_invalid"]), ] @@ -535,7 +579,7 @@ def test_get_events_timestamps(newer_than, older_than): @pytest.mark.parametrize("users", test_invalid_users) @pytest.mark.parametrize("newer_than", test_invalid_newer_than) def test_get_events_invalid_data( - topics, + topics, project_names, states, users, @@ -545,25 +589,26 @@ def test_get_events_invalid_data( of invalid input and prevent errors or unexpected results. Verifies: - - Confirms that the result is either empty or aligns with expected valid - entries: + - Confirms that the result is either empty or aligns with expected + valid entries: - `topics`: Result is empty or topics is set to `None`. - - `project_names`: Result is empty or project names exist in the + - `project_names`: Result is empty or project names exist in the list of valid project names. - `states`: Result is empty or states is set to `None`. - `users`: Result is empty or each user exists as a valid user. - - `newer_than`: Result is empty or `newer_than` date is in the past. + - `newer_than`: Result is empty or `newer_than` date is in the + past. Note: - - Adjusts the timeout setting if necessary to handle a large number + - Adjusts the timeout setting if necessary to handle a large number of tests and avoid timeout errors. - + """ if get_timeout() < 5: set_timeout(None) # default timeout value res = list(get_events( - topics=topics, + topics=topics, project_names=project_names, states=states, users=users, @@ -573,10 +618,13 @@ def test_get_events_invalid_data( valid_project_names = get_project_names() assert res == [] \ - or topics is None + or topics is None assert res == [] \ or project_names is None \ - or any(project_name in valid_project_names for project_name in project_names) + or any( + project_name in valid_project_names + for project_name in project_names + ) assert res == [] \ or states is None assert res == [] \ @@ -589,19 +637,19 @@ def test_get_events_invalid_data( @pytest.fixture def event_id(): - """Fixture that retrieves the ID of a recent event created within + """Fixture that retrieves the ID of a recent event created within the last 5 days. Returns: - - The event ID of the most recent event within the last 5 days + - The event ID of the most recent event within the last 5 days if available. - `None` if no recent events are found within this time frame. - + """ - recent_event = list(get_events( + recent_events = list(get_events( newer_than=(datetime.now(timezone.utc) - timedelta(days=5)).isoformat() )) - return recent_event[0]["id"] if recent_event else None + return recent_events[0]["id"] if recent_events else None test_update_sender = [ ("test.server.api"), @@ -621,7 +669,7 @@ def event_id(): ] test_update_description = [ - ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce viverra."), + ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vivera."), ("Updated description test...") ] @@ -648,24 +696,26 @@ def test_update_event( payload=None, progress=None, ): - """Verifies that the `update_event` function correctly updates event fields. + """Verifies that the `update_event` function correctly updates event + fields. Verifies: - The function updates the specified event fields based on the provided - parameters (`sender`, `username`, `status`, `description`, `retries`, - etc.). - - Only the fields specified in `kwargs` are updated, and other fields + parameters (`sender`, `username`, `status`, `description`, + `retries`, etc.). + - Only the fields specified in `kwargs` are updated, and other fields remain unchanged. - - The `updatedAt` field is updated and the change occurs within a - reasonable time frame (within one minute). - - The event's state before and after the update matches the expected + - The `updatedAt` field is updated and the change occurs within + a reasonable time frame (within one minute). + - The event's state before and after the update matches the expected values for the updated fields. - + Notes: - - Parameters like `event_id`, `sender`, `username`, `status`, - `description`, `retries`, etc., are passed dynamically to the function. + - Parameters like `event_id`, `sender`, `username`, `status`, + `description`, `retries`, etc., are passed dynamically to + the function. - If any parameter is `None`, it is excluded from the update request. - + """ kwargs = { key: value @@ -693,7 +743,9 @@ def test_update_event( or key in kwargs.keys() and value == kwargs.get(key) \ or ( key == "updatedAt" and ( - datetime.fromisoformat(value) - datetime.now(timezone.utc) < timedelta(minutes=1) + (datetime.fromisoformat(value) - datetime.now(timezone.utc)) + < + timedelta(minutes=1) ) ) @@ -708,13 +760,14 @@ def test_update_event( @pytest.mark.parametrize("status", test_update_invalid_status) def test_update_event_invalid_status(status): - """Tests `update_event` with invalid status values to ensure correct + """Tests `update_event` with invalid status values to ensure correct error handling for unsupported status inputs. Verifies: - - Confirms that an `HTTPRequestError` is raised for invalid status values - when attempting to update an event with an unsupported status. - + - Confirms that an `HTTPRequestError` is raised for invalid status + values when attempting to update an event with an unsupported + status. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, status=status) @@ -730,19 +783,19 @@ def test_update_event_invalid_status(status): @pytest.mark.parametrize("progress", test_update_invalid_progress) def test_update_event_invalid_progress(event_id, progress): - """Tests `update_event` with invalid progress values to ensure correct + """Tests `update_event` with invalid progress values to ensure correct error handling for unsupported progress inputs. Verifies: - - Confirms that an `HTTPRequestError` is raised for invalid progress values - when attempting to update an event with unsupported progress. - + - Confirms that an `HTTPRequestError` is raised for invalid progress + values when attempting to update an event with unsupported + progress. + """ with pytest.raises(exceptions.HTTPRequestError): update_event(event_id, progress=progress) - TEST_SOURCE_TOPIC = "test.source.topic" TEST_TARGET_TOPIC = "test.target.topic" DEFAULT_NUMBER_OF_EVENTS = 3 @@ -755,9 +808,9 @@ def test_update_event_invalid_progress(event_id, progress): @pytest.fixture def clean_up_events(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): - """Called at the beginning to close any pending events that may interfere with - the test setup or outcomes by marking them as 'finished'. - + """Used before running marked testt to close any pending events that may + interfere with the test setup or outcomes by marking them as 'finished'. + """ events = list(get_events(topics=topics)) for event in events: @@ -767,41 +820,40 @@ def clean_up_events(topics=[TEST_SOURCE_TOPIC, TEST_TARGET_TOPIC]): @pytest.fixture def create_test_events(num_of_events=DEFAULT_NUMBER_OF_EVENTS): - """Fixture to create a specified number of test events and return their IDs. - - This fixture dispatches events to the `TEST_SOURCE_TOPIC` and returns the - list of event IDs for the created events. + """This fixture dispatches events to the `TEST_SOURCE_TOPIC` and returns + the list of event IDs for the created events. """ return [ - dispatch_event(topic=TEST_SOURCE_TOPIC, sender="tester", description=f"New test event n. {num}")["id"] + dispatch_event( + topic=TEST_SOURCE_TOPIC, + sender="tester", + description=f"New test event n. {num}" + )["id"] for num in range(num_of_events) ] -# clean_up_events should be below create_test_events to ensure it is called first -# pytest probably does not guarantee the order of execution +# clean_up should be below create_test to ensure it is called first +# pytest probably does not guarantee the order of execution @pytest.mark.usefixtures("create_test_events") @pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job(sequential): - """Tests the `enroll_event_job` function for proper event job enrollment and sequential behavior. + """Tests the `enroll_event_job` function for proper event job enrollment + based on sequential argument. Verifies: - - `enroll_event_job` correctly creates and returns a job with specified parameters - (`source_topic`, `target_topic`, `sender`, and `sequential`). - - When `sequential` is set to `True`, only one job can be enrolled at a time, - preventing new enrollments until the first job is closed or updated. - - When `sequential` is `False` or `None`, multiple jobs can be enrolled - concurrently without conflicts. - - The `update_event` function successfully updates the `status` of a job - as expected, allowing for sequential job processing. - - Parameters: - new_events: Fixture or setup to initialize new events for the test case. + - When `sequential` is set to `True`, only one job can be enrolled at + a time, preventing new enrollments until the first job is closed or + updated. + - When `sequential` is `False` or `None`, multiple jobs can be + enrolled concurrently without conflicts. + - The `update_event` function updates the `status` of a job to allowing + next sequential job processing. Notes: - - `update_event` is used to set `job_1`'s status to "failed" to test + - `update_event` is used to set `job_1`'s status to "failed" to test re-enrollment behavior. - TODO - delete events after test if possible @@ -840,22 +892,23 @@ def test_enroll_event_job(sequential): @pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_failed(sequential): - """Tests `enroll_event_job` behavior when the initial job fails and sequential processing is enabled. + """Tests `enroll_event_job` behavior when the initial job fails and + sequential processing is enabled. Verifies: - - `enroll_event_job` creates a job (`job_1`) with specified parameters - (`source_topic`, `target_topic`, `sender`, and `sequential`). - - After `job_1` fails (status set to "failed"), a new job (`job_2`) can be - enrolled with the same parameters. - - When `sequential` is `True`, the test verifies that `job_1` and `job_2` - are identical, as a failed sequential job should not allow a new job - to be enrolled separately. - - When `sequential` is `False`, `job_1` and `job_2` are allowed to differ, - as concurrent processing is permitted. + - `enroll_event_job` creates a job (`job_1`) with specified parameters + `(`source_topic`, `target_topic`, `sender`, and `sequential`). + - After `job_1` fails (status set to "failed"), a new job (`job_2`) can + be enrolled with the same parameters. + - When `sequential` is `True`, the test verifies that `job_1` and + `job_2` are identical, as a failed sequential job should not allow + a new job to be enrolled separately. + - When `sequential` is `False`, `job_1` and `job_2` are allowed to + differ, as concurrent processing is permitted. Notes: - - `update_event` is used to set `job_1`'s status to "failed" to test - re-enrollment behavior. + - `update_event` is used to set `job_1`'s status to "failed" to test + re-enrollment behavior. - TODO - delete events after test if possible """ @@ -881,17 +934,15 @@ def test_enroll_event_job_failed(sequential): @pytest.mark.usefixtures("clean_up_events") @pytest.mark.parametrize("sequential", test_sequential) def test_enroll_event_job_same_sender(sequential): - """Tests `enroll_event_job` behavior when multiple jobs are enrolled by the same sender. + """Tests `enroll_event_job` behavior when multiple jobs are enrolled + by the same sender. Verifies: - - `enroll_event_job` creates a job (`job_1`) with specified parameters - (`source_topic`, `target_topic`, `sender`, and `sequential`). - - When a second job (`job_2`) is enrolled by the same sender with - identical parameters, the function should return the same job as `job_1` - (indicating idempotent behavior for the same sender and parameters). - - The test checks that `job_1` and `job_2` are identical, ensuring that - no duplicate jobs are created for the same sender when `sequential` - behavior does not permit additional jobs. + - `enroll_event_job` creates a `job_1` and `job_2` with the same + parameters (`source_topic`, `target_topic`, `sender`, and + `sequential`). + - The test checks that `job_1` and `job_2` are identical, ensuring that + no duplicate jobs are created for the same sender. Notes: - TODO - delete events after test if possible @@ -914,34 +965,32 @@ def test_enroll_event_job_same_sender(sequential): assert job_1 == job_2 -test_invalid_topics = [ - (("invalid_source_topic", "invalid_target_topic")), - (("nonexisting_source_topic", "nonexisting_target_topic")), +test_invalid_topic = [ + ("invalid_source_topic"), + ("nonexisting_source_topic"), ] @pytest.mark.usefixtures("clean_up_events") -@pytest.mark.parametrize("topics", test_invalid_topics) +@pytest.mark.parametrize("topic", test_invalid_topics) @pytest.mark.parametrize("sequential", test_sequential) -def test_enroll_event_job_invalid_topics(topics, sequential): +def test_enroll_event_job_invalid_topic(topic, sequential): """Tests `enroll_event_job` behavior when provided with invalid topics. Verifies: - - `enroll_event_job` returns `None` when given invalid `source_topic` - or `target_topic`, indicating that the function properly rejects - invalid topic values. - - The function correctly handles both sequential and non-sequential - job processing modes when invalid topics are used. + - `enroll_event_job` returns `None` when given invalid `source_topic` + or `target_topic`, indicating that the function properly rejects + invalid topic values. + - The function correctly handles both sequential and non-sequential + job processing modes when invalid topics are used. Notes: - - `clean_up_events()` is called at the beginning to close any pending jobs that - may interfere with the test setup or outcomes. - + - `clean_up_events()` is called at the beginning to close any pending + jobs that may interfere with the test setup or outcomes. + """ - source_topic, target_topic = topics - job = enroll_event_job( - source_topic=source_topic, - target_topic=target_topic, + source_topic=topic, + target_topic=TEST_TARGET_TOPIC, sender="test_sender", sequential=sequential ) @@ -949,31 +998,28 @@ def test_enroll_event_job_invalid_topics(topics, sequential): assert job is None -# clean_up_events should be below create_test_events to ensure it is called first -# pytest probably does not guarantee the order of execution +# clean_up should be below create_test to ensure it is called first +# pytest probably does not guarantee the order of execution @pytest.mark.usefixtures("create_test_events") @pytest.mark.usefixtures("clean_up_events") def test_enroll_event_job_sequential_false(): """Tests `enroll_event_job` behavior when `sequential` is set to `False`. Verifies: - - `enroll_event_job` creates a unique job for each sender even when - `sequential` is set to `False`, allowing concurrent job processing. - - Each job has a unique `dependsOn` identifier, ensuring that no two - jobs are linked in dependency, as expected for non-sequential enrollment. - - Parameters: - new_events: Fixture or setup to initialize new events for the test case. - + - `enroll_event_job` creates a unique job for each sender even when + `sequential` is set to `False`, allowing concurrent job processing. + - Each job has a unique `dependsOn` identifier + Notes: - - The `depends_on_ids` set is used to track `dependsOn` identifiers and - verify that each job has a unique dependency state, as required for + - The `depends_on_ids` set is used to track `dependsOn` identifiers and + verify that each job has a unique dependency state, as required for concurrent processing. - TODO - delete events after test if possible + """ depends_on_ids = set() - for sender in ["test_1", "test_2", "test_3"]: + for sender in ["tester_1", "tester_2", "tester_3"]: job = enroll_event_job( source_topic=TEST_SOURCE_TOPIC, target_topic=TEST_TARGET_TOPIC, @@ -997,31 +1043,33 @@ def test_thumbnail_operations( project_code=TEST_PROJECT_CODE, thumbnail_path=AYON_THUMBNAIL_PATH ): - """Tests thumbnail operations for a project, including creation, association, retrieval, and verification. + """Tests thumbnail operations for a project. Verifies: - - A project is created with a specified name and code, and any existing - project with the same name is deleted before setup to ensure a clean state. - A thumbnail is created for the project and associated with a folder. - - The thumbnail associated with the folder is correctly retrieved, with - attributes matching the project name and thumbnail ID. - - The content of the retrieved thumbnail matches the expected image bytes - read from the specified `thumbnail_path`. + - The thumbnail associated with the folder is correctly retrieved, with + attributes matching the project name and thumbnail ID. + - The content of the retrieved thumbnail matches the expected image + bytes read from the specified `thumbnail_path`. Notes: - - `delete_project` is called initially to remove any pre-existing project - with the same name, ensuring no conflicts during testing. + - `delete_project` is called initially to remove any pre-existing + project with the same name, ensuring no conflicts during testing. - At the end of the test, the project is deleted to clean up resources. - + """ if get_project(project_name): delete_project(TEST_PROJECT_NAME) project = create_project(project_name, project_code) - + thumbnail_id = create_thumbnail(project_name, thumbnail_path) - folder_id = create_folder(project_name, "my_test_folder", thumbnail_id=thumbnail_id) + folder_id = create_folder( + project_name, + "my_test_folder", + thumbnail_id=thumbnail_id + ) thumbnail = get_folder_thumbnail(project_name, folder_id, thumbnail_id) assert thumbnail.project_name == project_name @@ -1036,37 +1084,41 @@ def test_thumbnail_operations( def test_addon_methods(): - """Tests addon methods, including upload, verification, download, and cleanup of addon resources. + """Tests addon methods, including upload and download of private file. Verifies: - - An addon with the specified name and version does not exist at the start. + - An addon with the specified name and version does not exist at the + start. - Uploads an addon package `.zip` file and triggers a server restart. - - Ensures the server restart completes, and verifies the uploaded addon is - available in the list of addons after the restart. - - Downloads a private file associated with the addon, verifying its - existence and correct download location. - - Cleans up downloaded files and directories after the test to maintain a - clean state. + - Ensures the server restart completes, and verifies the uploaded addon + is available in the list of addons after the restart. + - Downloads a private file associated with the addon, verifying its + existence and correct download location. + - Cleans up downloaded files and directories after the test to maintain + a clean state. Notes: - - `time.sleep()` is used to allow for a brief pause for the server restart. - - The `finally` block removes downloaded files and the directory to prevent - residual test artifacts. + - `time.sleep()` is used to allow for a brief pause for the server + restart. + - The `finally` block removes downloaded files and the directory to + prevent residual test artifacts. """ addon_name = "tests" addon_version = "1.0.0" download_path = "tests/resources/tmp_downloads" private_file_path = os.path.join(download_path, "ayon-symbol.png") - + delete(f"/addons/{addon_name}/{addon_version}") - assert all(addon_name != addon["name"] for addon in get_addons_info()["addons"]) + assert all( + addon_name != addon["name"] for addon in get_addons_info()["addons"] + ) try: _ = upload_addon_zip("tests/resources/addon/package/tests-1.0.0.zip") - + trigger_server_restart() - + # need to wait at least 0.1 sec. to restart server time.sleep(0.5) while True: @@ -1101,18 +1153,18 @@ def api_artist_user(): """Fixture that sets up an API connection for a non-admin artist user. Workflow: - - Checks if the project exists; if not, it creates one with specified - `TEST_PROJECT_NAME` and `TEST_PROJECT_CODE`. - - Establishes a server API connection and retrieves the list of available - access groups. - - Configures a new user with limited permissions (`isAdmin` and `isManager` - set to `False`) and assigns all available access groups as default and - project-specific groups. - - Creates a new API connection using the artist user's credentials - (`username` and `password`) and logs in with it. + - Checks if the project exists; if not, it creates one with specified + `TEST_PROJECT_NAME` and `TEST_PROJECT_CODE`. + - Establishes a server API connection and retrieves the list + of available access groups. + - Configures a new user with limited permissions (`isAdmin` and + `isManager` set to `False`) and assigns all available access groups + as default and project-specific groups. + - Creates a new API connection using the artist user's credentials + (`username` and `password`) and logs in with it. Returns: - new_api: A `ServerAPI` instance authenticated with the artist user's + new_api: A `ServerAPI` instance authenticated with the artist user's credentials, ready to use in tests. """ @@ -1121,7 +1173,7 @@ def api_artist_user(): project = create_project(TEST_PROJECT_NAME, TEST_PROJECT_CODE) api = get_server_api_connection() - + username = "testUser" password = "testUserPassword" response = api.get("accessGroups/_") @@ -1148,19 +1200,19 @@ def api_artist_user(): def test_server_restart_as_user(api_artist_user): - """Tests that a non-admin artist user is not permitted to trigger a server restart. + """Tests that a non-admin artist user is not permitted to trigger a server + restart. Verifies: - - An attempt to call `trigger_server_restart` as a non-admin artist user - raises an exception, ensuring that only users with the appropriate - permissions (e.g., admins) can perform server restart operations. + - An attempt to call `trigger_server_restart` as a non-admin artist + user raises an exception, ensuring that only users with the + appropriate permissions (e.g., admins) can perform server restart + operations. Notes: - - The test checks the access control around the `trigger_server_restart` - method to confirm that only authorized users can perform critical actions - like server restarts. + - The exception is not specified as there is a todo to raise more + specific exception. """ with pytest.raises(Exception): api_artist_user.trigger_server_restart() - From 8f19047a6e738530b5b210cd2dd301e1cead2cd8 Mon Sep 17 00:00:00 2001 From: Tadeas Hejnic Date: Thu, 14 Nov 2024 16:42:21 +0100 Subject: [PATCH 16/16] Code adjust for pass the linting check --- tests/resources/addon/package.py | 2 +- tests/resources/addon/server/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/resources/addon/package.py b/tests/resources/addon/package.py index 649526135..2143cebcd 100644 --- a/tests/resources/addon/package.py +++ b/tests/resources/addon/package.py @@ -6,4 +6,4 @@ # ayon_launcher_version = ">=1.0.2" ayon_required_addons = {} -ayon_compatible_addons = {} \ No newline at end of file +ayon_compatible_addons = {} diff --git a/tests/resources/addon/server/__init__.py b/tests/resources/addon/server/__init__.py index 0f794cbcf..e330d93a2 100644 --- a/tests/resources/addon/server/__init__.py +++ b/tests/resources/addon/server/__init__.py @@ -9,11 +9,11 @@ def initialize(self): self.get_test, method="GET", ) - + async def get_test( self, user: CurrentUser, ): """Return a random folder from the database""" return { "success": True, - } + }