From 61fcaca06e8a1ba850575551d1b839c040e3a9a7 Mon Sep 17 00:00:00 2001 From: DanChov Date: Mon, 22 Dec 2025 12:12:53 +0100 Subject: [PATCH 01/32] independent test runs --- mergin/test/test_client.py | 345 ++++++++++++++++++++----------------- 1 file changed, 191 insertions(+), 154 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 07cdeec..fc4356b 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -60,19 +60,54 @@ def get_limit_overrides(storage: int): return {"storage": storage, "projects": 2, "api_allowed": True} +def create_project_path(name, mc): + return (mc._user_info.get('username') + "/" + name) + @pytest.fixture(scope="function") def mc(): - client = create_client(API_USER, USER_PWD) + assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" + + suffix = int(datetime.now(timezone.utc).timestamp()) + user = f"apitest_{suffix}@example.com" + password = "testpass123" + + anon_client = MerginClient(SERVER_URL) + anon_client.post( + "/v1/tests/users", + {"email": user, "password": password}, + json_headers, + validate_auth=False, + ) + + client = create_client(user, password) create_workspace_for_client(client) - return client + yield client + #cleanup @pytest.fixture(scope="function") def mc2(): - client = create_client(API_USER2, USER_PWD2) + assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" + + suffix = int(datetime.now(timezone.utc).timestamp()) + user = f"apitest2_{suffix}@example.com" + password = "testpass123" + + anon_client = MerginClient(SERVER_URL) + anon_client.post( + "/v1/tests/users", + {"email": user, "password": password}, + json_headers, + validate_auth=False, + ) + + client = create_client(user, password) create_workspace_for_client(client) - return client + yield client + + #cleanup + @pytest.fixture(scope="function") @@ -188,7 +223,7 @@ def test_login(mc): def test_create_delete_project(mc: MerginClient): test_project = "test_create_delete" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -196,52 +231,52 @@ def test_create_delete_project(mc: MerginClient): # create new (empty) project on server mc.create_project(test_project) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) # try again with pytest.raises(ClientError, match=f"already exists"): mc.create_project(test_project) # remove project - mc.delete_project_now(API_USER + "/" + test_project) + mc.delete_project_now(project) projects = mc.projects_list(flag="created") - assert not any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert not any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) # try again, nothing to delete with pytest.raises(ClientError): - mc.delete_project_now(API_USER + "/" + test_project) + mc.delete_project_now(project) # test that using namespace triggers deprecate warning, but creates project correctly with pytest.deprecated_call(match=r"The usage of `namespace` parameter in `create_project\(\)` is deprecated."): - mc.create_project(test_project, namespace=API_USER) + mc.create_project(test_project, namespace=mc.username()) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) mc.delete_project_now(project) # test that using only project name triggers deprecate warning, but creates project correctly with pytest.deprecated_call(match=r"The use of only project name in `create_project\(\)` is deprecated"): mc.create_project(test_project) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) mc.delete_project_now(project) # test that even if project is specified with full name and namespace is specified a warning is raised, but still create project correctly with pytest.warns(UserWarning, match="Parameter `namespace` specified with full project name"): - mc.create_project(project, namespace=API_USER) + mc.create_project(project, namespace=mc.username()) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) mc.delete_project_now(project) # test that create project with full name works mc.create_project(project) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) mc.delete_project_now(project) def test_create_remote_project_from_local(mc): test_project = "test_project" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -254,23 +289,23 @@ def test_create_remote_project_from_local(mc): # verify we have correct metadata source_mp = MerginProject(project_dir) - assert source_mp.project_full_name() == f"{API_USER}/{test_project}" + assert source_mp.project_full_name() == f"{mc.username()}/{test_project}" assert source_mp.project_name() == test_project - assert source_mp.workspace_name() == API_USER + assert source_mp.workspace_name() == mc.username() assert source_mp.version() == "v1" # check basic metadata about created project project_info = mc.project_info(project) assert project_info["version"] == "v1" assert project_info["name"] == test_project - assert project_info["namespace"] == API_USER + assert project_info["namespace"] == mc.username() assert project_info["id"] == source_mp.project_id() # check project metadata retrieval by id project_info = mc.project_info(source_mp.project_id()) assert project_info["version"] == "v1" assert project_info["name"] == test_project - assert project_info["namespace"] == API_USER + assert project_info["namespace"] == mc.username() assert project_info["id"] == source_mp.project_id() version = mc.project_version_info(project_info.get("id"), "v1") @@ -293,7 +328,7 @@ def test_create_remote_project_from_local(mc): def test_push_pull_changes(mc): test_project = "test_push" - project = API_USER + "/" + test_project + project= create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir @@ -306,9 +341,9 @@ def test_push_pull_changes(mc): mc.download_project(project, project_dir_2) mp2 = MerginProject(project_dir_2) - assert mp2.project_full_name() == f"{API_USER}/{test_project}" + assert mp2.project_full_name() == f"{mc.username()}/{test_project}" assert mp2.project_name() == test_project - assert mp2.workspace_name() == API_USER + assert mp2.workspace_name() == mc.username() assert mp2.version() == "v1" # test push changes (add, remove, rename, update) @@ -340,7 +375,7 @@ def test_push_pull_changes(mc): mc.push_project(project_dir) mp = MerginProject(project_dir) - assert mp.project_full_name() == f"{API_USER}/{test_project}" + assert mp.project_full_name() == f"{mc.username()}/{test_project}" assert mp.version() == "v2" project_info = mc.project_info(project) @@ -379,9 +414,9 @@ def test_push_pull_changes(mc): assert not os.path.exists(os.path.join(project_dir_2, f_removed)) assert not os.path.exists(os.path.join(project_dir_2, f_renamed)) assert os.path.exists(os.path.join(project_dir_2, "renamed.txt")) - assert os.path.exists(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1))) + assert os.path.exists(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, mc.username(), 1))) assert ( - generate_checksum(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1))) + generate_checksum(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, mc.username(), 1))) == f_conflict_checksum ) assert generate_checksum(os.path.join(project_dir_2, f_updated)) == f_remote_checksum @@ -393,7 +428,7 @@ def test_cancel_push(mc): finished. """ test_project = "test_cancel_push" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project + "_3") # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_4") cleanup(mc, project, [project_dir, project_dir_2]) @@ -434,7 +469,7 @@ def test_cancel_push(mc): def test_ignore_files(mc): test_project = "test_blacklist" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -453,7 +488,7 @@ def test_ignore_files(mc): def test_sync_diff(mc): test_project = f"test_sync_diff" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir with no changes project_dir_3 = os.path.join(TMP_DIR, test_project + "_3") # concurrent project dir with local changes @@ -523,7 +558,7 @@ def test_list_of_push_changes(mc): } test_project = "test_list_of_push_changes" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -542,7 +577,7 @@ def test_list_of_push_changes(mc): def test_token_renewal(mc): """Test token regeneration in case it has expired.""" test_project = "test_token_renewal" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -557,7 +592,7 @@ def test_token_renewal(mc): def test_force_gpkg_update(mc): test_project = "test_force_update" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -595,7 +630,7 @@ def test_new_project_sync(mc): """Create a new project, download it, add a file and then do sync - it should not fail""" test_project = "test_new_project_sync" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -624,7 +659,7 @@ def test_missing_basefile_pull(mc): """ test_project = "test_missing_basefile_pull" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), test_project) @@ -661,7 +696,7 @@ def test_empty_file_in_subdir(mc): """Test pull of a project where there is an empty file in a sub-directory""" test_project = "test_empty_file_in_subdir" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), test_project) @@ -690,7 +725,7 @@ def test_empty_file_in_subdir(mc): def test_clone_project(mc: MerginClient): test_project = "test_clone_project" - test_project_fullname = API_USER + "/" + test_project + test_project_fullname = create_project_path(test_project,mc) # cleanups project_dir = os.path.join(TMP_DIR, test_project) @@ -699,98 +734,98 @@ def test_clone_project(mc: MerginClient): # create new (empty) project on server mc.create_project(test_project) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == test_project and p["namespace"] == API_USER) + assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) cloned_project_name = test_project + "_cloned" - test_cloned_project_fullname = API_USER + "/" + cloned_project_name + test_cloned_project_fullname = create_project_path(cloned_project_name,mc) # cleanup cloned project cloned_project_dir = os.path.join(TMP_DIR, cloned_project_name) - cleanup(mc, API_USER + "/" + cloned_project_name, [cloned_project_dir]) + cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) # clone specifying cloned_project_namespace, does clone but raises deprecation warning with pytest.deprecated_call(match=r"The usage of `cloned_project_namespace` parameter in `clone_project\(\)`"): - mc.clone_project(test_project_fullname, cloned_project_name, API_USER) + mc.clone_project(test_project_fullname, cloned_project_name, mc.username()) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == API_USER) - cleanup(mc, API_USER + "/" + cloned_project_name, [cloned_project_dir]) + assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == mc.username()) + cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) # clone without specifying cloned_project_namespace relies on workspace with user name, does clone but raises deprecation warning with pytest.deprecated_call(match=r"The use of only project name as `cloned_project_name` in `clone_project\(\)`"): mc.clone_project(test_project_fullname, cloned_project_name) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == API_USER) - cleanup(mc, API_USER + "/" + cloned_project_name, [cloned_project_dir]) + assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == mc.username()) + cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) # clone project with full cloned project name with specification of `cloned_project_namespace` raises warning with pytest.warns(match=r"Parameter `cloned_project_namespace` specified with full cloned project name"): - mc.clone_project(test_project_fullname, test_cloned_project_fullname, API_USER) + mc.clone_project(test_project_fullname, test_cloned_project_fullname, mc.username()) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == API_USER) - cleanup(mc, API_USER + "/" + cloned_project_name, [cloned_project_dir]) + assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == mc.username()) + cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) # clone project using project full name mc.clone_project(test_project_fullname, test_cloned_project_fullname) projects = mc.projects_list(flag="created") - assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == API_USER) - cleanup(mc, API_USER + "/" + cloned_project_name, [cloned_project_dir]) + assert any(p for p in projects if p["name"] == cloned_project_name and p["namespace"] == mc.username()) + cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) -def test_set_read_write_access(mc): +def test_set_read_write_access(mc,mc2): test_project = "test_set_read_write_access" - test_project_fullname = API_USER + "/" + test_project + test_project_fullname = create_project_path(test_project,mc) # cleanups - project_dir = os.path.join(TMP_DIR, test_project, API_USER) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mc, test_project_fullname, [project_dir]) # create new (empty) project on server mc.create_project(test_project) # Add writer access to another client - project_info = get_project_info(mc, API_USER, test_project) + project_info = get_project_info(mc, mc.username(), test_project) access = project_info["access"] - access["writersnames"].append(API_USER2) - access["readersnames"].append(API_USER2) + access["writersnames"].append(mc2.username()) + access["readersnames"].append(mc2.username()) editor_support = server_has_editor_support(mc, access) if editor_support: - access["editorsnames"].append(API_USER2) + access["editorsnames"].append(mc2.username()) mc.set_project_access(test_project_fullname, access) - project_info = get_project_info(mc, API_USER, test_project) + project_info = get_project_info(mc, mc.username(), test_project) access = project_info["access"] - assert API_USER2 in access["writersnames"] - assert API_USER2 in access["readersnames"] + assert mc2.username() in access["writersnames"] + assert mc2.username() in access["readersnames"] if editor_support: - assert API_USER2 in access["editorsnames"] + assert mc2.username() in access["editorsnames"] -def test_set_editor_access(mc): +def test_set_editor_access(mc,mc2): test_project = "test_set_editor_access" - test_project_fullname = API_USER + "/" + test_project + test_project_fullname = create_project_path(test_project,mc) # cleanups - project_dir = os.path.join(TMP_DIR, test_project, API_USER) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mc, test_project_fullname, [project_dir]) # create new (empty) project on server mc.create_project(test_project) - project_info = get_project_info(mc, API_USER, test_project) + project_info = get_project_info(mc, mc.username(), test_project) access = project_info["access"] # Stop test if server does not support editor access if not server_has_editor_support(mc, access): return - access["readersnames"].append(API_USER2) - access["editorsnames"].append(API_USER2) + access["readersnames"].append(mc2.username()) + access["editorsnames"].append(mc2.username()) mc.set_project_access(test_project_fullname, access) # check access - project_info = get_project_info(mc, API_USER, test_project) + project_info = get_project_info(mc, mc.username(), test_project) access = project_info["access"] - assert API_USER2 in access["editorsnames"] - assert API_USER2 in access["readersnames"] - assert API_USER2 not in access["writersnames"] + assert mc2.username() in access["editorsnames"] + assert mc2.username() in access["readersnames"] + assert mc2.username() not in access["writersnames"] def test_available_workspace_storage(mcStorage): @@ -874,10 +909,10 @@ def test_available_storage_validation2(mc, mc2): - both accounts should ideally have a free plan """ test_project = "test_available_storage_validation2" - test_project_fullname = API_USER2 + "/" + test_project + test_project_fullname = create_project_path(test_project,mc2) # cleanups - project_dir = os.path.join(TMP_DIR, test_project, API_USER) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mc, test_project_fullname, [project_dir]) cleanup(mc2, test_project_fullname, [project_dir]) @@ -885,10 +920,10 @@ def test_available_storage_validation2(mc, mc2): mc2.create_project(test_project) # Add writer access to another client - project_info = get_project_info(mc2, API_USER2, test_project) + project_info = get_project_info(mc2, mc2.username(), test_project) access = project_info["access"] - access["writersnames"].append(API_USER) - access["readersnames"].append(API_USER) + access["writersnames"].append(mc.username()) + access["readersnames"].append(mc.username()) mc2.set_project_access(test_project_fullname, access) # download project @@ -947,8 +982,8 @@ def _generate_big_file(filepath, size): def test_get_projects_by_name(mc): """Test server 'bulk' endpoint for projects' info""" test_projects = { - "projectA": f"{API_USER}/projectA", - "projectB": f"{API_USER}/projectB", + "projectA": f"{mc.username()}/projectA", + "projectB": f"{mc.username()}/projectB", } for name, full_name in test_projects.items(): @@ -965,7 +1000,7 @@ def test_get_projects_by_name(mc): def test_download_versions(mc): test_project = "test_download" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # download dirs project_dir_v1 = os.path.join(TMP_DIR, test_project + "_v1") @@ -1004,7 +1039,7 @@ def test_paginated_project_list(mc): test_projects = dict() for symb in "ABCDEF": name = f"test_paginated_{symb}" - test_projects[name] = f"{API_USER}/{name}" + test_projects[name] = f"{mc.username()}/{name}" for name, full_name in test_projects.items(): cleanup(mc, full_name, []) @@ -1044,7 +1079,7 @@ def test_missing_local_file_pull(mc): test_project = "test_dir" file_to_remove = "test2.txt" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project + "_5") # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_6") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data", test_project) @@ -1085,7 +1120,7 @@ def test_logging(mc): def create_versioned_project(mc, project_name, project_dir, updated_file, remove=True, overwrite=False): - project = API_USER + "/" + project_name + project = create_project_path(project_name,mc) cleanup(mc, project, [project_dir]) # create remote project @@ -1126,7 +1161,7 @@ def create_versioned_project(mc, project_name, project_dir, updated_file, remove def test_get_versions_with_file_changes(mc): """Test getting versions where the file was changed.""" test_project = "test_file_modified_versions" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" @@ -1171,7 +1206,7 @@ def check_gpkg_same_content(mergin_project, gpkg_path_1, gpkg_path_2): def test_download_file(mc): """Test downloading single file at specified versions.""" test_project = "test_download_file" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" @@ -1203,7 +1238,7 @@ def test_download_file(mc): def test_download_diffs(mc): """Test download diffs for a project file between specified project versions.""" test_project = "test_download_diffs" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(project_dir, "diffs") # project for downloading files at various versions f_updated = "base.gpkg" @@ -1247,9 +1282,9 @@ def test_download_diffs(mc): assert "Available versions: [1, 2, 3, 4]" in str(e.value) -def test_modify_project_permissions(mc): +def test_modify_project_permissions(mc,mc2): test_project = "test_project" - test_project_fullname = API_USER + "/" + test_project + test_project_fullname = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -1260,33 +1295,33 @@ def test_modify_project_permissions(mc): # create remote project mc.create_project_and_push(test_project_fullname, directory=project_dir) - mc.add_user_permissions_to_project(test_project_fullname, [API_USER], "owner") + mc.add_user_permissions_to_project(test_project_fullname, [mc.username()], "owner") permissions = mc.project_user_permissions(test_project_fullname) - assert permissions["owners"] == [API_USER] - assert permissions["writers"] == [API_USER] - assert permissions["readers"] == [API_USER] + assert permissions["owners"] == [mc.username()] + assert permissions["writers"] == [mc.username()] + assert permissions["readers"] == [mc.username()] editor_support = server_has_editor_support(mc, permissions) if editor_support: assert permissions["editors"] == [API_USER] - mc.add_user_permissions_to_project(test_project_fullname, [API_USER2], "writer") + mc.add_user_permissions_to_project(test_project_fullname, [mc2.username()], "writer") permissions = mc.project_user_permissions(test_project_fullname) - assert set(permissions["owners"]) == {API_USER} - assert set(permissions["writers"]) == {API_USER, API_USER2} - assert set(permissions["readers"]) == {API_USER, API_USER2} + assert set(permissions["owners"]) == {mc.username()} + assert set(permissions["writers"]) == {mc.username(), mc2.username()} + assert set(permissions["readers"]) == {mc.username(), mc2.username()} editor_support = server_has_editor_support(mc, permissions) if editor_support: - assert set(permissions["editors"]) == {API_USER, API_USER2} + assert set(permissions["editors"]) == {mc.username(), mc2.username()} - mc.remove_user_permissions_from_project(test_project_fullname, [API_USER2]) + mc.remove_user_permissions_from_project(test_project_fullname, [mc2.username()]) permissions = mc.project_user_permissions(test_project_fullname) - assert permissions["owners"] == [API_USER] - assert permissions["writers"] == [API_USER] - assert permissions["readers"] == [API_USER] + assert permissions["owners"] == [mc.username()] + assert permissions["writers"] == [mc.username()] + assert permissions["readers"] == [mc.username()] editor_support = server_has_editor_support(mc, permissions) if editor_support: - assert permissions["editors"] == [API_USER] + assert permissions["editors"] == [mc.username()] def _use_wal(db_file): @@ -1391,7 +1426,7 @@ def test_push_gpkg_schema_change(mc): """ test_project = "test_push_gpkg_schema_change" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) test_gpkg = os.path.join(project_dir, "test.gpkg") test_gpkg_basefile = os.path.join(project_dir, ".mergin", "test.gpkg") @@ -1446,6 +1481,8 @@ def test_push_gpkg_schema_change(mc): # at this point we still have an open sqlite connection to the GPKG, so checkpointing will not work correctly) mc.push_project(project_dir) + subprocess.run(["sleep", "15"]) + # WITH TWO SQLITE copies: fails here (sqlite3.OperationalError: disk I/O error) + in geodiff log: SQLITE3: (283)recovered N frames from WAL file _check_test_table(test_gpkg) @@ -1471,12 +1508,12 @@ def test_rebase_local_schema_change(mc, extra_connection): test_project = "test_rebase_local_schema_change" if extra_connection: test_project += "_extra_conn" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") test_gpkg_basefile = os.path.join(project_dir, ".mergin", "test.gpkg") - test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 1) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, mc.username(), 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1536,13 +1573,13 @@ def test_rebase_remote_schema_change(mc, extra_connection): test_project = "test_rebase_remote_schema_change" if extra_connection: test_project += "_extra_conn" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") test_gpkg_2 = os.path.join(project_dir_2, "test.gpkg") test_gpkg_basefile = os.path.join(project_dir, ".mergin", "test.gpkg") - test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 1) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, mc.username(), 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1601,7 +1638,7 @@ def test_rebase_success(mc, extra_connection): test_project = "test_rebase_success" if extra_connection: test_project += "_extra_conn" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") @@ -1823,7 +1860,7 @@ def test_unfinished_pull(mc): unfinished_pull directory is created with the content of the server changes. """ test_project = "test_unfinished_pull" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir unfinished_pull_dir = os.path.join( @@ -1832,7 +1869,7 @@ def test_unfinished_pull(mc): test_gpkg = os.path.join(project_dir, "test.gpkg") test_gpkg_2 = os.path.join(project_dir_2, "test.gpkg") test_gpkg_basefile = os.path.join(project_dir, ".mergin", "test.gpkg") - test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 2) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, mc.username(), 2) test_gpkg_unfinished_pull = os.path.join(project_dir, ".mergin", "unfinished_pull", "test.gpkg") cleanup(mc, project, [project_dir, project_dir_2]) @@ -1911,7 +1948,7 @@ def test_unfinished_pull_push(mc): in the unfinished pull state. """ test_project = "test_unfinished_pull_push" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir unfinished_pull_dir = os.path.join( @@ -1920,7 +1957,7 @@ def test_unfinished_pull_push(mc): test_gpkg = os.path.join(project_dir, "test.gpkg") test_gpkg_2 = os.path.join(project_dir_2, "test.gpkg") test_gpkg_basefile = os.path.join(project_dir, ".mergin", "test.gpkg") - test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 2) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, mc.username(), 2) test_gpkg_unfinished_pull = os.path.join(project_dir, ".mergin", "unfinished_pull", "test.gpkg") cleanup(mc, project, [project_dir, project_dir_2]) @@ -2007,7 +2044,7 @@ def test_unfinished_pull_push(mc): def test_project_versions_list(mc): """Test getting project versions in various ranges""" test_project = "test_project_versions" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) create_versioned_project(mc, test_project, project_dir, "base.gpkg") project_info = mc.project_info(project) @@ -2046,7 +2083,7 @@ def test_project_versions_list(mc): def test_report(mc): test_project = "test_report" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" mp = create_versioned_project(mc, test_project, project_dir, f_updated, remove=False, overwrite=True) @@ -2080,7 +2117,7 @@ def test_report(mc): ] ) assert headers in content - assert f"base.gpkg,simple,{API_USER}" in content + assert f"base.gpkg,simple,{mc.username()}" in content assert "v3,update,,,2" in content # files not edited are not in reports assert "inserted_1_A.gpkg" not in content @@ -2104,10 +2141,10 @@ def test_user_permissions(mc, mc2): Test retrieving user permissions """ test_project = "test_permissions" - test_project_fullname = API_USER2 + "/" + test_project + test_project_fullname = create_project_path(test_project,mc2) # cleanups - project_dir = os.path.join(TMP_DIR, test_project, API_USER) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mc, test_project_fullname, [project_dir]) cleanup(mc2, test_project_fullname, [project_dir]) @@ -2115,18 +2152,18 @@ def test_user_permissions(mc, mc2): mc2.create_project(test_project) # Add reader access to another client - project_info = get_project_info(mc2, API_USER2, test_project) + project_info = get_project_info(mc2, mc2.username(), test_project) access = project_info["access"] - access["readersnames"].append(API_USER) + access["readersnames"].append(mc.username()) mc2.set_project_access(test_project_fullname, access) # reader should not have write access assert not mc.has_writing_permissions(test_project_fullname) # Add writer access to another client - project_info = get_project_info(mc2, API_USER2, test_project) + project_info = get_project_info(mc2, mc2.username(), test_project) access = project_info["access"] - access["writersnames"].append(API_USER) + access["writersnames"].append(mc.username()) mc2.set_project_access(test_project_fullname, access) # now user shold have write access @@ -2141,7 +2178,7 @@ def test_report_failure(mc): and then deleted. """ test_project = "test_report_failure" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = os.path.join(project_dir, "test.gpkg") report_file = os.path.join(TMP_DIR, "report.csv") @@ -2177,7 +2214,7 @@ def test_changesets_download(mc): changesets are cached. """ test_project = "test_changesets_download" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = "test.gpkg" file_path = os.path.join(project_dir, "test.gpkg") @@ -2222,7 +2259,7 @@ def test_changesets_download(mc): def test_version_info(mc): """Check retrieving detailed information about single project version.""" test_project = "test_version_info" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = "test.gpkg" file_path = os.path.join(project_dir, test_gpkg) @@ -2240,10 +2277,10 @@ def test_version_info(mc): mc.push_project(project_dir) project_info = mc.project_info(project) info = mc.project_version_info(project_info.get("id"), "v2") - assert info["namespace"] == API_USER + assert info["namespace"] == mc.username() assert info["project_name"] == test_project assert info["name"] == "v2" - assert info["author"] == API_USER + assert info["author"] == mc.username() created = datetime.strptime(info["created"], "%Y-%m-%dT%H:%M:%SZ") assert created.date() == date.today() assert info["changes"]["updated"][0]["size"] == 98304 @@ -2251,7 +2288,7 @@ def test_version_info(mc): def test_clean_diff_files(mc): test_project = "test_clean" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir @@ -2278,7 +2315,7 @@ def test_clean_diff_files(mc): def test_reset_local_changes(mc: MerginClient): test_project = f"test_reset_local_changes" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_v2") # primary project dir for updates @@ -2399,7 +2436,7 @@ def test_reset_local_changes(mc: MerginClient): def test_project_metadata(mc): test_project = "test_project_metadata" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) @@ -2413,16 +2450,16 @@ def test_project_metadata(mc): # rewrite metadata nemespace to prevent failing tests with other user than test_plugin with open(metadata_file, "r") as f: metadata = json.load(f) - metadata["name"] = f"{API_USER}/{test_project}" + metadata["name"] = f"{mc.username()}/{test_project}" project_metadata_file = os.path.join(project_dir, ".mergin", "mergin.json") with open(project_metadata_file, "w") as f: json.dump(metadata, f, indent=2) # verify we have correct metadata mp = MerginProject(project_dir) - assert mp.project_full_name() == f"{API_USER}/{test_project}" + assert mp.project_full_name() == f"{mc.username()}/{test_project}" assert mp.project_name() == test_project - assert mp.workspace_name() == API_USER + assert mp.workspace_name() == mc.username() assert mp.version() == "v0" # copy metadata in new format @@ -2430,15 +2467,15 @@ def test_project_metadata(mc): # rewrite metadata nemespace to prevent failing tests with other user than test_plugin with open(metadata_file, "r") as f: metadata = json.load(f) - metadata["namespace"] = API_USER + metadata["namespace"] = mc.username() with open(project_metadata_file, "w") as f: json.dump(metadata, f, indent=2) # verify we have correct metadata mp = MerginProject(project_dir) - assert mp.project_full_name() == f"{API_USER}/{test_project}" + assert mp.project_full_name() == f"{mc.username()}/{test_project}" assert mp.project_name() == test_project - assert mp.workspace_name() == API_USER + assert mp.workspace_name() == mc.username() assert mp.version() == "v0" @@ -2447,8 +2484,8 @@ def test_project_rename(mc: MerginClient): test_project = "test_project_rename" test_project_renamed = "test_project_renamed" - project = API_USER + "/" + test_project - project_renamed = API_USER + "/" + test_project_renamed + project = create_project_path(test_project,mc) + project_renamed = create_project_path(test_project_renamed,mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir @@ -2469,7 +2506,7 @@ def test_project_rename(mc: MerginClient): project_info = mc.project_info(project_renamed) assert project_info["version"] == "v1" assert project_info["name"] == test_project_renamed - assert project_info["namespace"] == API_USER + assert project_info["namespace"] == mc.username() with pytest.raises(ClientError, match="The requested URL was not found on the server"): mc.project_info(project) @@ -2485,7 +2522,7 @@ def test_project_rename(mc: MerginClient): # cannot rename project that does not exist with pytest.raises(ClientError, match="The requested URL was not found on the server."): - mc.rename_project(API_USER + "/" + "non_existing_project", "new_project") + mc.rename_project(mc.username() + "/" + "non_existing_project", "new_project") # cannot rename with full project name with pytest.raises( @@ -2498,7 +2535,7 @@ def test_project_rename(mc: MerginClient): def test_download_files(mc: MerginClient): """Test downloading files at specified versions.""" test_project = "test_download_files" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" download_dir = os.path.join(TMP_DIR, "test-download-files-tmp") @@ -2561,7 +2598,7 @@ def test_download_files(mc: MerginClient): def test_download_failure(mc): test_project = "test_download_failure" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -2628,15 +2665,15 @@ def test_editor_push(mc: MerginClient, mc2: MerginClient): if not mc.has_editor_support(): return test_project_name = "test_editor_push" - test_project_fullname = API_USER + "/" + test_project_name + test_project_fullname = create_project_path(test_project_name,mc) project_dir = os.path.join(TMP_DIR, test_project_name) project_dir2 = os.path.join(TMP_DIR, test_project_name + "_2") cleanup(mc, test_project_fullname, [project_dir, project_dir2]) # create new (empty) project on server # TODO: return project_info from create project, don't use project_full name for project info, instead returned id of project - mc.create_project(test_project_name) - project_info = get_project_info(mc, API_USER, test_project_name) + mc.create_project(test_project_fullname) + project_info = get_project_info(mc, mc.username(), test_project_name) mc.add_project_collaborator(project_info["id"], mc2.username(), ProjectRole.EDITOR) # download empty project mc2.download_project(test_project_fullname, project_dir) @@ -2714,13 +2751,13 @@ def test_error_push_already_named_project(mc: MerginClient): assert e.value.detail == "Project with the same name already exists" assert e.value.http_error == 409 assert e.value.http_method == "POST" - assert e.value.url == f"{mc.url}v1/project/{API_USER}" + assert e.value.url == f"{mc.url}v1/project/{mc.username()}" def test_error_projects_limit_hit(mcStorage: MerginClient): test_project = "project_above_projects_limit" test_project_fullname = STORAGE_WORKSPACE + "/" + test_project - project_dir = os.path.join(TMP_DIR, test_project, API_USER) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mcStorage, test_project, [project_dir]) client_workspace = None @@ -2748,11 +2785,11 @@ def test_error_projects_limit_hit(mcStorage: MerginClient): assert e.value.url == f"{mcStorage.url}v1/project/testpluginstorage" -def test_workspace_requests(mc2: MerginClient): +def test_workspace_requests(mc: MerginClient, mc2): test_project = "test_permissions" - test_project_fullname = API_USER2 + "/" + test_project - - project_info = mc2.project_info(test_project_fullname) + test_project_fullname = create_project_path(test_project,mc) + mc.create_project(test_project_fullname) + project_info = mc.project_info(test_project_fullname) ws_id = project_info.get("workspace_id") usage = mc2.workspace_usage(ws_id) @@ -2806,11 +2843,11 @@ def test_access_management(mc: MerginClient, mc2: MerginClient): mc2.update_workspace_member(workspace_id, new_user["id"], ws_role) # add project test_project_name = "test_collaborators" - test_project_fullname = API_USER + "/" + test_project_name - project_dir = os.path.join(TMP_DIR, test_project_name, API_USER) + test_project_fullname = mc.username() + "/" + test_project_name + project_dir = os.path.join(TMP_DIR, test_project_name, mc.username()) cleanup(mc, test_project_fullname, [project_dir]) mc.create_project(test_project_name) - project_info = get_project_info(mc, API_USER, test_project_name) + project_info = get_project_info(mc, mc.username(), test_project_name) test_project_id = project_info["id"] project_role = ProjectRole.READER # user must be added to project collaborators before updating project role @@ -2877,7 +2914,7 @@ def test_server_config(mc: MerginClient): def test_send_logs(mc: MerginClient, monkeypatch): """Test that logs can be send to the server.""" test_project = "test_logs_send" - project = API_USER + "/" + test_project + project = create_project_path(test_project,mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) @@ -2982,7 +3019,7 @@ def test_validate_auth(mc: MerginClient): # ----- Client with token and username/password ----- # create a client with valid auth token based on other MerginClient instance with username/password that allows relogin if the token is expired mc_auth_token_login = MerginClient( - SERVER_URL, auth_token=mc._auth_session["token"], login=API_USER, password=USER_PWD + SERVER_URL, auth_token=mc._auth_session["token"], login=mc.username(), password="testpass123" ) # this should pass and not raise an error @@ -2998,7 +3035,7 @@ def test_validate_auth(mc: MerginClient): # create a client with valid auth token based on other MerginClient instance with username and WRONG password # that does NOT allow relogin if the token is expired mc_auth_token_login_wrong_password = MerginClient( - SERVER_URL, auth_token=mc._auth_session["token"], login=API_USER, password="WRONG_PASSWORD" + SERVER_URL, auth_token=mc._auth_session["token"], login=mc.username(), password="WRONG_PASSWORD" ) # this should pass and not raise an error From 4b0a25e0d98387ac67c90697d79d0e648e1e7eae Mon Sep 17 00:00:00 2001 From: DanChov Date: Mon, 5 Jan 2026 10:28:00 +0100 Subject: [PATCH 02/32] dom commit vscode --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6b48311..2f0fdd1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ htmlcov .pytest_cache deps venv -.vscode/settings.json +.vscode/ \ No newline at end of file From c556623e039a2272040c507d1204c37d906a9fc4 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 19 Jan 2026 11:36:18 +0100 Subject: [PATCH 03/32] Fix version of black to latest --- .github/workflows/code_style.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 7e4bf94..a9a6841 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -9,5 +9,7 @@ jobs: - uses: actions/checkout@v2 - uses: psf/black@stable with: + # bump this version as needed + version: 26.1.0 options: "--check --diff --verbose -l 120" - src: "./mergin" \ No newline at end of file + src: "./mergin" From 74fb4be4b038ff3bd856b0382cce18a71814ba58 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 19 Jan 2026 11:44:53 +0100 Subject: [PATCH 04/32] Upgrade version to 0.12.0 - fix update_version.py script to upgrade also setup.py --- mergin/version.py | 2 +- scripts/update_version.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mergin/version.py b/mergin/version.py index eddb700..49a8046 100644 --- a/mergin/version.py +++ b/mergin/version.py @@ -1,5 +1,5 @@ # The version is also stored in ../setup.py -__version__ = "0.11.0" +__version__ = "0.12.0" # There seems to be no single nice way to keep version info just in one place: # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/scripts/update_version.py b/scripts/update_version.py index 2020659..c565fd5 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -26,4 +26,4 @@ def replace_in_file(filepath, regex, sub): setup_file = os.path.join(dir_path, os.pardir, "setup.py") print("patching " + setup_file) -replace_in_file(setup_file, "version='.*", "version='" + ver + "',") +replace_in_file(setup_file, 'version=".*"', 'version="' + ver + '"') diff --git a/setup.py b/setup.py index da2d05a..a8184e6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.11.0", + version="0.12.0", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.", From a464f3cd300249ba30acb3c5a4516f1129999498 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 23 Jan 2026 12:17:41 +0100 Subject: [PATCH 05/32] Upgrade geodiff to 2.1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a8184e6..8d610b7 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ platforms="any", install_requires=[ "python-dateutil==2.8.2", - "pygeodiff==2.0.4", + "pygeodiff==2.1.1", "pytz==2022.1", "click==8.1.3", ], From 0380c3c87bc6dbb07322850fa5f15f9e527fae92 Mon Sep 17 00:00:00 2001 From: DanChov Date: Mon, 2 Feb 2026 09:58:12 +0100 Subject: [PATCH 06/32] Independent test runs creating new user and workspace every time test run --- mergin/test/test_client.py | 228 +++++++++++++++++++------------------ 1 file changed, 116 insertions(+), 112 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index fc4356b..272c7bc 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -60,16 +60,16 @@ def get_limit_overrides(storage: int): return {"storage": storage, "projects": 2, "api_allowed": True} + def create_project_path(name, mc): - return (mc._user_info.get('username') + "/" + name) + return mc.username() + "/" + name @pytest.fixture(scope="function") def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - suffix = int(datetime.now(timezone.utc).timestamp()) - user = f"apitest_{suffix}@example.com" + user = f"apitest_{create_random_suffix()}@example.com" password = "testpass123" anon_client = MerginClient(SERVER_URL) @@ -77,21 +77,24 @@ def mc(): "/v1/tests/users", {"email": user, "password": password}, json_headers, - validate_auth=False, + validate_auth=False, ) - + client = create_client(user, password) + info = client.user_info() + user_id = info["id"] create_workspace_for_client(client) + yield client - #cleanup + anon_client.delete(f"/v1/tests/users/{user_id}", validate_auth=False) + @pytest.fixture(scope="function") def mc2(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - suffix = int(datetime.now(timezone.utc).timestamp()) - user = f"apitest2_{suffix}@example.com" + user = f"apitest2_{create_random_suffix()}@example.com" password = "testpass123" anon_client = MerginClient(SERVER_URL) @@ -99,39 +102,31 @@ def mc2(): "/v1/tests/users", {"email": user, "password": password}, json_headers, - validate_auth=False, + validate_auth=False, ) - + client = create_client(user, password) + info = client.user_info() + user_id = info["id"] create_workspace_for_client(client) yield client - #cleanup + anon_client.delete(f"/v1/tests/users/{user_id}", validate_auth=False) +def create_user_in_workspace(workspace_id: int, mc: MerginClient, role: WorkspaceRole): + client = mc.create_user( + email=f"mc2UserInWorkspace{create_random_suffix()}@example.com", + password="Testpass123", + workspace_id=workspace_id, + workspace_role=role, + ) -@pytest.fixture(scope="function") -def mcStorage(request): - client = create_client(API_USER, USER_PWD) - workspace_name = create_workspace_for_client(client, STORAGE_WORKSPACE) - client_workspace = None - for workspace in client.workspaces_list(): - if workspace["name"] == workspace_name: - client_workspace = workspace - break - client_workspace_id = client_workspace["id"] - client_workspace_storage = client_workspace["storage"] + return MerginClient(url=SERVER_URL, login=client["email"], password="Testpass123") - def teardown(): - # back to original values... (1 project, api allowed ...) - client.patch( - f"/v1/tests/workspaces/{client_workspace_id}", - {"limits_override": get_limit_overrides(client_workspace_storage)}, - {"Content-Type": "application/json"}, - ) - request.addfinalizer(teardown) - return client +def create_random_suffix(): + return int(datetime.now(timezone.utc).timestamp() * random.randrange(1, 13)) def create_client(user, pwd): @@ -223,7 +218,7 @@ def test_login(mc): def test_create_delete_project(mc: MerginClient): test_project = "test_create_delete" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -328,7 +323,7 @@ def test_create_remote_project_from_local(mc): def test_push_pull_changes(mc): test_project = "test_push" - project= create_project_path(test_project, mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir @@ -428,7 +423,7 @@ def test_cancel_push(mc): finished. """ test_project = "test_cancel_push" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project + "_3") # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_4") cleanup(mc, project, [project_dir, project_dir_2]) @@ -469,7 +464,7 @@ def test_cancel_push(mc): def test_ignore_files(mc): test_project = "test_blacklist" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -488,7 +483,7 @@ def test_ignore_files(mc): def test_sync_diff(mc): test_project = f"test_sync_diff" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir with no changes project_dir_3 = os.path.join(TMP_DIR, test_project + "_3") # concurrent project dir with local changes @@ -558,7 +553,7 @@ def test_list_of_push_changes(mc): } test_project = "test_list_of_push_changes" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -577,7 +572,7 @@ def test_list_of_push_changes(mc): def test_token_renewal(mc): """Test token regeneration in case it has expired.""" test_project = "test_token_renewal" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -592,7 +587,7 @@ def test_token_renewal(mc): def test_force_gpkg_update(mc): test_project = "test_force_update" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -630,7 +625,7 @@ def test_new_project_sync(mc): """Create a new project, download it, add a file and then do sync - it should not fail""" test_project = "test_new_project_sync" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -659,7 +654,7 @@ def test_missing_basefile_pull(mc): """ test_project = "test_missing_basefile_pull" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), test_project) @@ -696,7 +691,7 @@ def test_empty_file_in_subdir(mc): """Test pull of a project where there is an empty file in a sub-directory""" test_project = "test_empty_file_in_subdir" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), test_project) @@ -725,7 +720,7 @@ def test_empty_file_in_subdir(mc): def test_clone_project(mc: MerginClient): test_project = "test_clone_project" - test_project_fullname = create_project_path(test_project,mc) + test_project_fullname = create_project_path(test_project, mc) # cleanups project_dir = os.path.join(TMP_DIR, test_project) @@ -737,7 +732,7 @@ def test_clone_project(mc: MerginClient): assert any(p for p in projects if p["name"] == test_project and p["namespace"] == mc.username()) cloned_project_name = test_project + "_cloned" - test_cloned_project_fullname = create_project_path(cloned_project_name,mc) + test_cloned_project_fullname = create_project_path(cloned_project_name, mc) # cleanup cloned project cloned_project_dir = os.path.join(TMP_DIR, cloned_project_name) @@ -771,9 +766,9 @@ def test_clone_project(mc: MerginClient): cleanup(mc, test_cloned_project_fullname, [cloned_project_dir]) -def test_set_read_write_access(mc,mc2): +def test_set_read_write_access(mc, mc2): test_project = "test_set_read_write_access" - test_project_fullname = create_project_path(test_project,mc) + test_project_fullname = create_project_path(test_project, mc) # cleanups project_dir = os.path.join(TMP_DIR, test_project, mc.username()) @@ -800,9 +795,9 @@ def test_set_read_write_access(mc,mc2): assert mc2.username() in access["editorsnames"] -def test_set_editor_access(mc,mc2): +def test_set_editor_access(mc, mc2): test_project = "test_set_editor_access" - test_project_fullname = create_project_path(test_project,mc) + test_project_fullname = create_project_path(test_project, mc) # cleanups project_dir = os.path.join(TMP_DIR, test_project, mc.username()) @@ -828,46 +823,42 @@ def test_set_editor_access(mc,mc2): assert mc2.username() not in access["writersnames"] -def test_available_workspace_storage(mcStorage): +def test_available_workspace_storage(mc: MerginClient): """ Testing of storage limit - applies to user pushing changes into own project (namespace matching username). This test also tests giving read and write access to another user. Additionally tests also uploading of big file. """ test_project = "test_available_workspace_storage" - test_project_fullname = STORAGE_WORKSPACE + "/" + test_project + test_project_fullname = create_project_path(test_project, mc) # cleanups - project_dir = os.path.join(TMP_DIR, test_project, API_USER) - cleanup(mcStorage, test_project_fullname, [project_dir]) + project_dir = os.path.join(TMP_DIR, test_project, mc.username()) + cleanup(mc, test_project_fullname, [project_dir]) # create new (empty) project on server # if namespace is not provided, function is creating project with username - mcStorage.create_project(test_project_fullname) + mc.create_project(test_project_fullname) # download project - mcStorage.download_project(test_project_fullname, project_dir) + mc.download_project(test_project_fullname, project_dir) # get info about storage capacity storage_remaining = 0 - client_workspace = None - for workspace in mcStorage.workspaces_list(): - if workspace["name"] == STORAGE_WORKSPACE: - client_workspace = workspace - break - assert client_workspace is not None + client_workspace = mc.workspaces_list()[0] # ask for alternative + current_storage = client_workspace["storage"] client_workspace_id = client_workspace["id"] # 5 MB testing_storage = 5242880 # add storage limit, to prevent creating too big files - mcStorage.patch( + mc.patch( f"/v1/tests/workspaces/{client_workspace_id}", {"limits_override": {"storage": testing_storage, "projects": 1, "api_allowed": True}}, {"Content-Type": "application/json"}, ) - if mcStorage.server_type() == ServerType.OLD: - user_info = mcStorage.user_info() + if mc.server_type() == ServerType.OLD: + user_info = mc.user_info() storage_remaining = testing_storage - user_info["disk_usage"] else: storage_remaining = testing_storage - client_workspace["disk_usage"] @@ -880,7 +871,7 @@ def test_available_workspace_storage(mcStorage): # try to upload got_right_err = False try: - mcStorage.push_project(project_dir) + mc.push_project(project_dir) except ClientError as e: # Expecting "You have reached a data limit" 400 server error msg. assert "You have reached a data limit" in str(e) @@ -889,7 +880,7 @@ def test_available_workspace_storage(mcStorage): assert got_right_err # Expecting empty project - project_info = get_project_info(mcStorage, STORAGE_WORKSPACE, test_project) + project_info = get_project_info(mc, client_workspace["name"], test_project) assert project_info["version"] == "v0" assert project_info["disk_usage"] == 0 @@ -909,7 +900,7 @@ def test_available_storage_validation2(mc, mc2): - both accounts should ideally have a free plan """ test_project = "test_available_storage_validation2" - test_project_fullname = create_project_path(test_project,mc2) + test_project_fullname = create_project_path(test_project, mc2) # cleanups project_dir = os.path.join(TMP_DIR, test_project, mc.username()) @@ -1000,7 +991,7 @@ def test_get_projects_by_name(mc): def test_download_versions(mc): test_project = "test_download" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # download dirs project_dir_v1 = os.path.join(TMP_DIR, test_project + "_v1") @@ -1079,7 +1070,7 @@ def test_missing_local_file_pull(mc): test_project = "test_dir" file_to_remove = "test2.txt" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project + "_5") # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_6") # concurrent project dir test_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data", test_project) @@ -1120,7 +1111,7 @@ def test_logging(mc): def create_versioned_project(mc, project_name, project_dir, updated_file, remove=True, overwrite=False): - project = create_project_path(project_name,mc) + project = create_project_path(project_name, mc) cleanup(mc, project, [project_dir]) # create remote project @@ -1161,7 +1152,7 @@ def create_versioned_project(mc, project_name, project_dir, updated_file, remove def test_get_versions_with_file_changes(mc): """Test getting versions where the file was changed.""" test_project = "test_file_modified_versions" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" @@ -1206,7 +1197,7 @@ def check_gpkg_same_content(mergin_project, gpkg_path_1, gpkg_path_2): def test_download_file(mc): """Test downloading single file at specified versions.""" test_project = "test_download_file" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" @@ -1238,7 +1229,7 @@ def test_download_file(mc): def test_download_diffs(mc): """Test download diffs for a project file between specified project versions.""" test_project = "test_download_diffs" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(project_dir, "diffs") # project for downloading files at various versions f_updated = "base.gpkg" @@ -1282,9 +1273,9 @@ def test_download_diffs(mc): assert "Available versions: [1, 2, 3, 4]" in str(e.value) -def test_modify_project_permissions(mc,mc2): +def test_modify_project_permissions(mc, mc2): test_project = "test_project" - test_project_fullname = create_project_path(test_project,mc) + test_project_fullname = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -1508,7 +1499,7 @@ def test_rebase_local_schema_change(mc, extra_connection): test_project = "test_rebase_local_schema_change" if extra_connection: test_project += "_extra_conn" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") @@ -1573,7 +1564,7 @@ def test_rebase_remote_schema_change(mc, extra_connection): test_project = "test_rebase_remote_schema_change" if extra_connection: test_project += "_extra_conn" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") @@ -1638,7 +1629,7 @@ def test_rebase_success(mc, extra_connection): test_project = "test_rebase_success" if extra_connection: test_project += "_extra_conn" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir test_gpkg = os.path.join(project_dir, "test.gpkg") @@ -1860,7 +1851,7 @@ def test_unfinished_pull(mc): unfinished_pull directory is created with the content of the server changes. """ test_project = "test_unfinished_pull" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir unfinished_pull_dir = os.path.join( @@ -1948,7 +1939,7 @@ def test_unfinished_pull_push(mc): in the unfinished pull state. """ test_project = "test_unfinished_pull_push" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir unfinished_pull_dir = os.path.join( @@ -2044,7 +2035,7 @@ def test_unfinished_pull_push(mc): def test_project_versions_list(mc): """Test getting project versions in various ranges""" test_project = "test_project_versions" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) create_versioned_project(mc, test_project, project_dir, "base.gpkg") project_info = mc.project_info(project) @@ -2083,7 +2074,7 @@ def test_project_versions_list(mc): def test_report(mc): test_project = "test_report" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" mp = create_versioned_project(mc, test_project, project_dir, f_updated, remove=False, overwrite=True) @@ -2141,7 +2132,7 @@ def test_user_permissions(mc, mc2): Test retrieving user permissions """ test_project = "test_permissions" - test_project_fullname = create_project_path(test_project,mc2) + test_project_fullname = create_project_path(test_project, mc2) # cleanups project_dir = os.path.join(TMP_DIR, test_project, mc.username()) @@ -2178,7 +2169,7 @@ def test_report_failure(mc): and then deleted. """ test_project = "test_report_failure" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = os.path.join(project_dir, "test.gpkg") report_file = os.path.join(TMP_DIR, "report.csv") @@ -2214,7 +2205,7 @@ def test_changesets_download(mc): changesets are cached. """ test_project = "test_changesets_download" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = "test.gpkg" file_path = os.path.join(project_dir, "test.gpkg") @@ -2259,7 +2250,7 @@ def test_changesets_download(mc): def test_version_info(mc): """Check retrieving detailed information about single project version.""" test_project = "test_version_info" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir test_gpkg = "test.gpkg" file_path = os.path.join(project_dir, test_gpkg) @@ -2288,7 +2279,7 @@ def test_version_info(mc): def test_clean_diff_files(mc): test_project = "test_clean" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_2") # concurrent project dir @@ -2315,7 +2306,7 @@ def test_clean_diff_files(mc): def test_reset_local_changes(mc: MerginClient): test_project = f"test_reset_local_changes" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_2 = os.path.join(TMP_DIR, test_project + "_v2") # primary project dir for updates @@ -2436,7 +2427,7 @@ def test_reset_local_changes(mc: MerginClient): def test_project_metadata(mc): test_project = "test_project_metadata" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) @@ -2484,8 +2475,8 @@ def test_project_rename(mc: MerginClient): test_project = "test_project_rename" test_project_renamed = "test_project_renamed" - project = create_project_path(test_project,mc) - project_renamed = create_project_path(test_project_renamed,mc) + project = create_project_path(test_project, mc) + project_renamed = create_project_path(test_project_renamed, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir @@ -2535,7 +2526,7 @@ def test_project_rename(mc: MerginClient): def test_download_files(mc: MerginClient): """Test downloading files at specified versions.""" test_project = "test_download_files" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) f_updated = "base.gpkg" download_dir = os.path.join(TMP_DIR, "test-download-files-tmp") @@ -2598,7 +2589,7 @@ def test_download_files(mc: MerginClient): def test_download_failure(mc): test_project = "test_download_failure" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) download_dir = os.path.join(TMP_DIR, "download", test_project) @@ -2660,12 +2651,21 @@ def test_editor(mc: MerginClient): assert sum(len(v) for v in qgs_changeset.values()) == 2 -def test_editor_push(mc: MerginClient, mc2: MerginClient): +def test_editor_push(mc: MerginClient): """Test push with editor""" + + test_project_name = "test_editor_push" + test_project_fullname = create_project_path(test_project_name, mc) + + mc.create_project(test_project_fullname) + project_info = mc.project_info(test_project_fullname) + mc_workspace_id = project_info.get("workspace_id") + + mc2 = create_user_in_workspace(mc_workspace_id, mc, WorkspaceRole.READER) + if not mc.has_editor_support(): return - test_project_name = "test_editor_push" - test_project_fullname = create_project_path(test_project_name,mc) + project_dir = os.path.join(TMP_DIR, test_project_name) project_dir2 = os.path.join(TMP_DIR, test_project_name + "_2") cleanup(mc, test_project_fullname, [project_dir, project_dir2]) @@ -2673,7 +2673,7 @@ def test_editor_push(mc: MerginClient, mc2: MerginClient): # create new (empty) project on server # TODO: return project_info from create project, don't use project_full name for project info, instead returned id of project mc.create_project(test_project_fullname) - project_info = get_project_info(mc, mc.username(), test_project_name) + project_info = mc.project_info(test_project_fullname) mc.add_project_collaborator(project_info["id"], mc2.username(), ProjectRole.EDITOR) # download empty project mc2.download_project(test_project_fullname, project_dir) @@ -2745,6 +2745,11 @@ def test_editor_push(mc: MerginClient, mc2: MerginClient): def test_error_push_already_named_project(mc: MerginClient): test_project = "test_push_already_existing" project_dir = os.path.join(TMP_DIR, test_project) + project_name = create_project_path(test_project, mc) + + remove_folders([project_dir]) + mc.create_project_and_push(project_name, project_dir) + shutil.rmtree(os.path.join(TMP_DIR, test_project, ".mergin"), ignore_errors=True) with pytest.raises(ClientError) as e: mc.create_project_and_push(test_project, project_dir) @@ -2754,27 +2759,24 @@ def test_error_push_already_named_project(mc: MerginClient): assert e.value.url == f"{mc.url}v1/project/{mc.username()}" -def test_error_projects_limit_hit(mcStorage: MerginClient): +def test_error_projects_limit_hit(mc: MerginClient): test_project = "project_above_projects_limit" - test_project_fullname = STORAGE_WORKSPACE + "/" + test_project + test_project_fullname = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project, mc.username()) - cleanup(mcStorage, test_project, [project_dir]) + cleanup(mc, test_project, [project_dir]) + + client_workspace = mc.workspaces_list()[0] # ask for alternative - client_workspace = None - for workspace in mcStorage.workspaces_list(): - if workspace["name"] == STORAGE_WORKSPACE: - client_workspace = workspace - break client_workspace_id = client_workspace["id"] client_workspace_storage = client_workspace["storage"] - mcStorage.patch( + mc.patch( f"/v1/tests/workspaces/{client_workspace_id}", {"limits_override": {**get_limit_overrides(client_workspace_storage), "projects": 0}}, {"Content-Type": "application/json"}, ) with pytest.raises(ClientError) as e: - mcStorage.create_project_and_push(test_project_fullname, project_dir) + mc.create_project_and_push(test_project_fullname, project_dir) assert e.value.server_code == ErrorCode.ProjectsLimitHit.value assert ( e.value.detail @@ -2782,21 +2784,23 @@ def test_error_projects_limit_hit(mcStorage: MerginClient): ) assert e.value.http_error == 422 assert e.value.http_method == "POST" - assert e.value.url == f"{mcStorage.url}v1/project/testpluginstorage" + assert e.value.url == f"{mc.url}v1/project/{mc.username()}" -def test_workspace_requests(mc: MerginClient, mc2): +def test_workspace_requests(mc: MerginClient): test_project = "test_permissions" - test_project_fullname = create_project_path(test_project,mc) + test_project_fullname = create_project_path(test_project, mc) mc.create_project(test_project_fullname) project_info = mc.project_info(test_project_fullname) ws_id = project_info.get("workspace_id") + mc2 = create_user_in_workspace(ws_id, mc, WorkspaceRole.OWNER) + usage = mc2.workspace_usage(ws_id) # Check type and common value assert type(usage) == dict assert usage["api"]["allowed"] == True - assert usage["history"]["quota"] == 214748364800 + assert usage["history"]["quota"] > 0 assert usage["history"]["usage"] == 0 service = mc2.workspace_service(ws_id) @@ -2806,7 +2810,7 @@ def test_workspace_requests(mc: MerginClient, mc2): assert service["plan"] assert service["plan"]["is_paid_plan"] == False assert service["plan"]["product_id"] == None - assert service["plan"]["type"] == "custom" + assert service["plan"]["type"] == "trial" assert service["subscription"] == None @@ -2914,7 +2918,7 @@ def test_server_config(mc: MerginClient): def test_send_logs(mc: MerginClient, monkeypatch): """Test that logs can be send to the server.""" test_project = "test_logs_send" - project = create_project_path(test_project,mc) + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) From 85a4fd9c4636c15090cb5bb1c31bcaf500f12005 Mon Sep 17 00:00:00 2001 From: DanChov Date: Mon, 2 Feb 2026 10:51:04 +0100 Subject: [PATCH 07/32] merge master --- .gitignore | 4 ---- mergin/test/test_client.py | 30 ------------------------------ 2 files changed, 34 deletions(-) diff --git a/.gitignore b/.gitignore index 0548252..adb34dd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,5 @@ htmlcov .pytest_cache deps venv -<<<<<<< HEAD -.vscode/ -======= debug.py .vscode/ ->>>>>>> master diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 9cf1564..91a6f0e 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -710,36 +710,6 @@ def test_force_gpkg_update(mc): assert "diff" not in f_remote -<<<<<<< HEAD -def test_new_project_sync(mc): - """Create a new project, download it, add a file and then do sync - it should not fail""" - - test_project = "test_new_project_sync" - project = create_project_path(test_project, mc) - project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates - - cleanup(mc, project, [project_dir]) - # create remote project - mc.create_project(test_project) - - # download the project - mc.download_project(project, project_dir) - - # add a test file - shutil.copy(os.path.join(TEST_DATA_DIR, "test.txt"), project_dir) - - # do a full sync - it should not fail - mc.pull_project(project_dir) - mc.push_project(project_dir) - - # make sure everything is up-to-date - mp = MerginProject(project_dir) - local_changes = mp.get_push_changes() - assert not local_changes["added"] and not local_changes["removed"] and not local_changes["updated"] - - -======= ->>>>>>> master def test_missing_basefile_pull(mc): """Test pull of a project where basefile of a .gpkg is missing for some reason (it should gracefully handle it by downloading the missing basefile) From 23bca5917465de7370fb64dfeb94a8e0300bf620 Mon Sep 17 00:00:00 2001 From: DanChov Date: Mon, 2 Feb 2026 13:19:30 +0100 Subject: [PATCH 08/32] Fixed tests from master to works on new test logic --- mergin/test/test_client.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 91a6f0e..bab2115 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -122,7 +122,7 @@ def mc2(): def create_user_in_workspace(workspace_id: int, mc: MerginClient, role: WorkspaceRole): client = mc.create_user( - email=f"mc2UserInWorkspace{create_random_suffix()}@example.com", + email=f"apitest_userInWorkspace_{create_random_suffix()}@example.com", password="Testpass123", workspace_id=workspace_id, workspace_role=role, @@ -335,7 +335,7 @@ def test_new_project_sync_v1_api(mc): server_features = mc.server_features() mc._server_features = {"v2_push_enabled": False} test_project = "test_new_project_sync_v1" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -365,7 +365,7 @@ def test_new_project_sync_v2_api(mc): This is using v2 endpoint. """ test_project = "test_new_project_sync_v2" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -490,7 +490,7 @@ def test_sync_remove(mc): In that case, there is not chunks creation (UploadQueueItems) and process is going directly to finalize. """ test_project = "test_sync_remove" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_removed = os.path.join(TMP_DIR, f"{test_project}_removed") # primary project dir for updates cleanup(mc, project, [project_dir, project_dir_removed]) @@ -3120,7 +3120,7 @@ def test_uploaded_chunks_cache(mc): if not mc.server_features().get("v2_push_enabled"): pytest.skip("Server does not support v2 push") test_project = "test_uploaded_chunks_cache" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) @@ -3158,7 +3158,7 @@ def test_client_push_project_async(mc): Integration tests for low level client_push functions """ test_project = "test_client_push" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates cleanup(mc, project, [project_dir]) # create remote project @@ -3186,7 +3186,7 @@ def test_client_pull_project_async(mc): Integration tests for low level client_pull functions """ test_project = "test_client_pull" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) # primary project dir for updates project_dir_pull = os.path.join(TMP_DIR, f"{test_project}_pull") # primary project dir for updates cleanup(mc, project, [project_dir, project_dir_pull]) @@ -3210,7 +3210,7 @@ def test_client_pull_project_async(mc): def test_client_project_sync(mc): test_project = "test_client_project_sync" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) project_dir_2 = os.path.join(TMP_DIR, f"{test_project}_2") cleanup(mc, project, [project_dir, project_dir_2]) @@ -3231,7 +3231,7 @@ def test_client_project_sync(mc): def test_client_project_sync_retry(mc): test_project = "test_client_project_sync_retry" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) mc.create_project(test_project) @@ -3265,7 +3265,7 @@ def test_client_project_sync_retry(mc): def test_push_file_limits(mc): test_project = "test_push_file_limits" - project = API_USER + "/" + test_project + project = create_project_path(test_project, mc) project_dir = os.path.join(TMP_DIR, test_project) cleanup(mc, project, [project_dir]) mc.create_project(test_project) From c6a118d76d3da0148594909ef58c0f2107be9b3b Mon Sep 17 00:00:00 2001 From: DanChov Date: Tue, 3 Feb 2026 17:07:04 +0100 Subject: [PATCH 09/32] Add option to run tests using predefined users --- .github/workflows/autotests.yml | 4 - README.md | 2 - mergin/test/test_client.py | 127 ++++++++++++++++++++++---------- 3 files changed, 87 insertions(+), 46 deletions(-) diff --git a/.github/workflows/autotests.yml b/.github/workflows/autotests.yml index f4c141f..70682ad 100644 --- a/.github/workflows/autotests.yml +++ b/.github/workflows/autotests.yml @@ -2,10 +2,6 @@ name: Auto Tests on: [push] env: TEST_MERGIN_URL: https://app.dev.merginmaps.com/ - TEST_API_USERNAME: test_plugin - TEST_API_PASSWORD: ${{ secrets.MERGINTEST_API_PASSWORD }} - TEST_API_USERNAME2: test_plugin2 - TEST_API_PASSWORD2: ${{ secrets.MERGINTEST_API_PASSWORD2 }} concurrency: group: ci-${{github.ref}}-autotests diff --git a/README.md b/README.md index d0e9844..55751bd 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,6 @@ For running test do: export TEST_API_PASSWORD= export TEST_API_USERNAME2= export TEST_API_PASSWORD2= - # workspace name with controlled available storage space (e.g. 20MB), default value: testpluginstorage - export TEST_STORAGE_WORKSPACE= pip install pytest pytest-cov coveralls pytest --cov-report html --cov=mergin mergin/test/ ``` diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index bab2115..2b978de 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -58,15 +58,26 @@ TMP_DIR = tempfile.gettempdir() TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") CHANGED_SCHEMA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "modified_schema") -STORAGE_WORKSPACE = os.environ.get("TEST_STORAGE_WORKSPACE", "testpluginstorage") json_headers = {"Content-Type": "application/json"} +def get_default_overrides(): + return {"projects": 100, "api_allowed": True} + + def get_limit_overrides(storage: int): return {"storage": storage, "projects": 2, "api_allowed": True} +def teardown(mc: MerginClient, client_workspace_id): + mc.patch( + f"/v1/tests/workspaces/{client_workspace_id}", + {"limits_override": get_default_overrides()}, + {"Content-Type": "application/json"}, + ) + + def create_project_path(name, mc): return mc.username() + "/" + name @@ -75,54 +86,81 @@ def create_project_path(name, mc): def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - user = f"apitest_{create_random_suffix()}@example.com" - password = "testpass123" + if API_USER != "" and USER_PWD != "": # if API_USER and USER_PWD is provided we log in user and use him for tests + user = API_USER + password = USER_PWD - anon_client = MerginClient(SERVER_URL) - anon_client.post( - "/v1/tests/users", - {"email": user, "password": password}, - json_headers, - validate_auth=False, - ) + client = create_client(user, password) + yield client - client = create_client(user, password) - info = client.user_info() - user_id = info["id"] - create_workspace_for_client(client) + else: # if user is not provided we create one + # user emial is generated with random + user = f"apitest_{create_random_suffix()}@example.com" + password = "testpass123" - yield client + anon_client = MerginClient(SERVER_URL) + anon_client.post( + "/v1/tests/users", + {"email": user, "password": password}, + json_headers, + validate_auth=False, + ) - anon_client.delete(f"/v1/tests/users/{user_id}", validate_auth=False) + # create MerginClient object and workspace for him + client = create_client(user, password) + create_workspace_for_client(client) + + # we yield client + yield client + + # afterwards we disable client and workspace + delete_test_user_and_workspace(client) @pytest.fixture(scope="function") def mc2(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - user = f"apitest2_{create_random_suffix()}@example.com" - password = "testpass123" + if API_USER2 != "" and USER_PWD2 != "": + user = API_USER2 + password = USER_PWD2 - anon_client = MerginClient(SERVER_URL) - anon_client.post( - "/v1/tests/users", - {"email": user, "password": password}, - json_headers, - validate_auth=False, - ) + client = create_client(user, password) + yield client + else: + user = f"apitest2_{create_random_suffix()}@example.com" + password = "testpass123" + + anon_client = MerginClient(SERVER_URL) + anon_client.post( + "/v1/tests/users", + {"email": user, "password": password}, + json_headers, + validate_auth=False, + ) + + client = create_client(user, password) + create_workspace_for_client(client) + + yield client - client = create_client(user, password) + delete_test_user_and_workspace(client) + + +def delete_test_user_and_workspace(client: MerginClient): + """Takes MC object and call delete /v1/tests/users/{user_id} which sets inactive_sice and set active to false.""" info = client.user_info() user_id = info["id"] - create_workspace_for_client(client) - yield client + anon_client = MerginClient(SERVER_URL) anon_client.delete(f"/v1/tests/users/{user_id}", validate_auth=False) def create_user_in_workspace(workspace_id: int, mc: MerginClient, role: WorkspaceRole): + """Creates user inside of workspace""" + client = mc.create_user( - email=f"apitest_userInWorkspace_{create_random_suffix()}@example.com", + email=f"apitest_userInWorkspace{create_random_suffix()}@example.com", password="Testpass123", workspace_id=workspace_id, workspace_role=role, @@ -906,7 +944,7 @@ def test_available_workspace_storage(mc: MerginClient): # get info about storage capacity storage_remaining = 0 - client_workspace = mc.workspaces_list()[0] # ask for alternative + client_workspace = mc.workspaces_list()[0] current_storage = client_workspace["storage"] client_workspace_id = client_workspace["id"] @@ -949,6 +987,8 @@ def test_available_workspace_storage(mc: MerginClient): # remove dummy big file from a disk remove_folders([project_dir]) + teardown(mc, client_workspace_id) + def test_available_storage_validation2(mc, mc2): """ @@ -2715,7 +2755,7 @@ def test_editor(mc: MerginClient): def test_editor_push(mc: MerginClient): """Test push with editor""" - test_project_name = "test_editor_push" + test_project_name = f"test_editor_push{create_random_suffix()}" test_project_fullname = create_project_path(test_project_name, mc) mc.create_project(test_project_fullname) @@ -2724,9 +2764,6 @@ def test_editor_push(mc: MerginClient): mc2 = create_user_in_workspace(mc_workspace_id, mc, WorkspaceRole.READER) - if not mc.has_editor_support(): - return - project_dir = os.path.join(TMP_DIR, test_project_name) project_dir2 = os.path.join(TMP_DIR, test_project_name + "_2") cleanup(mc, test_project_fullname, [project_dir, project_dir2]) @@ -2801,10 +2838,11 @@ def test_editor_push(mc: MerginClient): conflicted_file = project_file # There is no conflicted qgs file assert conflicted_file is None + delete_test_user_and_workspace(mc2) def test_error_push_already_named_project(mc: MerginClient): - test_project = "test_push_already_existing" + test_project = f"test_push_already_existing{create_random_suffix()}" project_dir = os.path.join(TMP_DIR, test_project) project_name = create_project_path(test_project, mc) @@ -2826,7 +2864,7 @@ def test_error_projects_limit_hit(mc: MerginClient): project_dir = os.path.join(TMP_DIR, test_project, mc.username()) cleanup(mc, test_project, [project_dir]) - client_workspace = mc.workspaces_list()[0] # ask for alternative + client_workspace = mc.workspaces_list()[0] client_workspace_id = client_workspace["id"] client_workspace_storage = client_workspace["storage"] @@ -2847,24 +2885,26 @@ def test_error_projects_limit_hit(mc: MerginClient): assert e.value.http_method == "POST" assert e.value.url == f"{mc.url}v1/project/{mc.username()}" + teardown(mc, client_workspace_id) + def test_workspace_requests(mc: MerginClient): - test_project = "test_permissions" + test_project = f"test_permissions{create_random_suffix()}" test_project_fullname = create_project_path(test_project, mc) mc.create_project(test_project_fullname) project_info = mc.project_info(test_project_fullname) ws_id = project_info.get("workspace_id") - mc2 = create_user_in_workspace(ws_id, mc, WorkspaceRole.OWNER) + user_in_workspace: MerginClient = create_user_in_workspace(ws_id, mc, WorkspaceRole.OWNER) - usage = mc2.workspace_usage(ws_id) + usage = user_in_workspace.workspace_usage(ws_id) # Check type and common value assert type(usage) == dict assert usage["api"]["allowed"] == True assert usage["history"]["quota"] > 0 assert usage["history"]["usage"] == 0 - service = mc2.workspace_service(ws_id) + service = user_in_workspace.workspace_service(ws_id) # Check type and common value assert type(service) == dict assert service["action_required"] == False @@ -2874,6 +2914,13 @@ def test_workspace_requests(mc: MerginClient): assert service["plan"]["type"] == "trial" assert service["subscription"] == None + info = user_in_workspace.user_info() + user_id = info["id"] + + # need to change role != OWNER because endpoint deletes workspaces where user is OWNER causing mc workspace to be disabled + user_in_workspace.update_workspace_member(ws_id, user_id, workspace_role=WorkspaceRole.READER) + delete_test_user_and_workspace(user_in_workspace) + def test_access_management(mc: MerginClient, mc2: MerginClient): # create a user in the workspace - From a0158d6e157602fa80422a429b680c05a74412f8 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 4 Feb 2026 17:57:14 +0100 Subject: [PATCH 10/32] Upgrade geodiff to 2.1.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8d610b7..4a57894 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ platforms="any", install_requires=[ "python-dateutil==2.8.2", - "pygeodiff==2.1.1", + "pygeodiff==2.1.2", "pytz==2022.1", "click==8.1.3", ], From 4ea17e60ea8f890c906f69f7033fac6eaf5b17e3 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 9 Feb 2026 12:41:42 +0100 Subject: [PATCH 11/32] Bump 0.12.1 --- mergin/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mergin/version.py b/mergin/version.py index 49a8046..73b936c 100644 --- a/mergin/version.py +++ b/mergin/version.py @@ -1,5 +1,5 @@ # The version is also stored in ../setup.py -__version__ = "0.12.0" +__version__ = "0.12.1" # There seems to be no single nice way to keep version info just in one place: # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/setup.py b/setup.py index 4a57894..fc7325c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.12.0", + version="0.12.1", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.", From ff0ce5eeb9e32da08029b6c867171d23f5de742f Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Tue, 10 Feb 2026 20:52:57 +0100 Subject: [PATCH 12/32] from compare file_set is coming diffs key as diffs are accidentaly stored in mergin.json --- mergin/local_changes.py | 2 ++ mergin/test/test_local_changes.py | 1 + 2 files changed, 3 insertions(+) diff --git a/mergin/local_changes.py b/mergin/local_changes.py index f0a235d..2b5ba8a 100644 --- a/mergin/local_changes.py +++ b/mergin/local_changes.py @@ -45,6 +45,8 @@ class FileChange: history: Optional[dict] = None # some functions (MerginProject.compare_file_sets) are adding location dict to the change from project info location: Optional[str] = None + # list of diff filenames associated with this change + diffs: Optional[List[str]] = None def get_diff(self) -> Optional[FileDiffChange]: if self.diff: diff --git a/mergin/test/test_local_changes.py b/mergin/test/test_local_changes.py index 2c3d878..4366f5d 100644 --- a/mergin/test/test_local_changes.py +++ b/mergin/test/test_local_changes.py @@ -28,6 +28,7 @@ def test_local_changes_from_dict(): "path": "base.gpkg", "size": 98304, "version": "v1", + "diffs": ["diff"], } ], } From 16f50b49e5ccf13933a9b562ab42690167802586 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Thu, 12 Feb 2026 14:38:01 +0100 Subject: [PATCH 13/32] Bump version 0.12.2 --- mergin/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mergin/version.py b/mergin/version.py index 73b936c..0e95ca8 100644 --- a/mergin/version.py +++ b/mergin/version.py @@ -1,5 +1,5 @@ # The version is also stored in ../setup.py -__version__ = "0.12.1" +__version__ = "0.12.2" # There seems to be no single nice way to keep version info just in one place: # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/setup.py b/setup.py index fc7325c..25b80b0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.12.1", + version="0.12.2", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.", From de1284649e351831c08194fc5432d74a205329d3 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 13 Feb 2026 12:30:07 +0100 Subject: [PATCH 14/32] Upgrade to geodiff at version 2.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 25b80b0..44ec72e 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ platforms="any", install_requires=[ "python-dateutil==2.8.2", - "pygeodiff==2.1.2", + "pygeodiff==2.2.0", "pytz==2022.1", "click==8.1.3", ], From a632fa91cc3c6a46554f74d222a38fc307faa4e1 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 13 Feb 2026 12:31:50 +0100 Subject: [PATCH 15/32] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0e9844..6a2d4bb 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ To download a specific version of a project: $ mergin --username john download --version v42 john/project1 ~/mergin/project1 ``` -To download a sepecific version of a single file: +To download a specific version of a single file: 1. First you need to download the project: ``` From f2ccb69283f649ac0a852c028fcdad824266f7e7 Mon Sep 17 00:00:00 2001 From: Marcel Kocisek Date: Mon, 16 Feb 2026 12:21:53 +0100 Subject: [PATCH 16/32] Clarify optional TEST_ variables in README Added note that TEST_ related variables are optional --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 55751bd..c1cc22b 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ For running test do: ```bash cd mergin + # TEST_ related vairables are optional export TEST_MERGIN_URL= # testing server export TEST_API_USERNAME= export TEST_API_PASSWORD= From 6a763208a0e1eec47f5b5ab4ae17ba58e9ccea90 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 16 Feb 2026 12:43:09 +0100 Subject: [PATCH 17/32] fix variables for user passwords --- mergin/test/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 2b978de..23bc024 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -86,7 +86,7 @@ def create_project_path(name, mc): def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - if API_USER != "" and USER_PWD != "": # if API_USER and USER_PWD is provided we log in user and use him for tests + if API_USER and USER_PWD: # if API_USER and USER_PWD is provided we log in user and use him for tests user = API_USER password = USER_PWD @@ -121,7 +121,7 @@ def mc(): def mc2(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - if API_USER2 != "" and USER_PWD2 != "": + if API_USER2 and USER_PWD2: # if API_USER2 and USER_PWD2 is provided we log in user and use him for tests user = API_USER2 password = USER_PWD2 From bc40f6505d45dd56169b2fd22de9ca0c8c661e59 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 17 Feb 2026 16:34:15 +0100 Subject: [PATCH 18/32] Add workflow to sync milestones to releases repo Co-Authored-By: Claude Opus 4.6 --- .github/workflows/notify-milestone.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/notify-milestone.yml diff --git a/.github/workflows/notify-milestone.yml b/.github/workflows/notify-milestone.yml new file mode 100644 index 0000000..b49f382 --- /dev/null +++ b/.github/workflows/notify-milestone.yml @@ -0,0 +1,20 @@ +name: Notify releases repo + +on: + milestone: + types: [created, edited] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Dispatch to releases repo + env: + GH_TOKEN: ${{ secrets.MM_RELEASES_SYNC_TOKEN }} + run: | + gh api repos/MerginMaps/releases/dispatches \ + --method POST \ + -f event_type=milestone-${{ github.event.action }} \ + -f 'client_payload[repo]=${{ github.repository }}' \ + -f 'client_payload[milestone_title]=${{ github.event.milestone.title }}' \ + -f 'client_payload[milestone_url]=${{ github.event.milestone.html_url }}' From b5e24edc60e3d80766082e464104e67ec5761057 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 19 Feb 2026 08:33:01 +0100 Subject: [PATCH 19/32] Mark security issues as safe for security checks --- mergin/merginproject.py | 4 ++-- mergin/test/test_client.py | 18 +++--------------- mergin/utils.py | 6 +++--- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/mergin/merginproject.py b/mergin/merginproject.py index e5e771c..ffd10dd 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -330,8 +330,8 @@ def compare_file_sets(self, origin, current): :Example: - >>> origin = [{'checksum': '08b0e8caddafe74bf5c11a45f65cedf974210fed', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}] - >>> current = [{'checksum': 'c9a4fd2afd513a97aba19d450396a4c9df8b2ba4', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}] + >>> origin = [{'checksum': '08b0e8caddafe74bf5c11a45f65cedf974210fed', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}] # pragma: allowlist secret + >>> current = [{'checksum': 'c9a4fd2afd513a97aba19d450396a4c9df8b2ba4', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}] # pragma: allowlist secret >>> self.compare_file_sets(origin, current) {"added": [{'checksum': 'c9a4fd2afd513a97aba19d450396a4c9df8b2ba4', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}], "removed": [[{'checksum': '08b0e8caddafe74bf5c11a45f65cedf974210fed', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}]], "renamed": [], "updated": []} diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 0b86ec3..e85b198 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -10,10 +10,8 @@ import pytest import pytz import sqlite3 -import glob -from unittest.mock import patch, Mock +from unittest.mock import patch -from unittest.mock import patch, Mock from .. import InvalidProject from ..client import ( @@ -1382,16 +1380,6 @@ def _create_spatial_table(db_file): cursor.execute("COMMIT;") -def _delete_spatial_table(db_file): - """Drops spatial table called 'test' in sqlite database. Useful to simulate change of database schema.""" - con = sqlite3.connect(db_file) - cursor = con.cursor() - cursor.execute("DROP TABLE poi;") - cursor.execute("DELETE FROM gpkg_geometry_columns WHERE table_name='poi';") - cursor.execute("DELETE FROM gpkg_contents WHERE table_name='poi';") - cursor.execute("COMMIT;") - - def _check_test_table(db_file): """Checks whether the 'test' table exists and has one row - otherwise fails with an exception.""" assert _get_table_row_count(db_file, "test") == 1 @@ -1401,7 +1389,7 @@ def _get_table_row_count(db_file, table): try: con_verify = sqlite3.connect(db_file) cursor_verify = con_verify.cursor() - cursor_verify.execute("select count(*) from {};".format(table)) + cursor_verify.execute("select count(*) from {};".format(table)) # nosec B608 return cursor_verify.fetchone()[0] finally: cursor_verify.close() @@ -3097,7 +3085,7 @@ def test_uploaded_chunks_cache(mc): with open(file, "rb") as file_handle: data = file_handle.read() - checksum = hashlib.sha1() + checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ checksum.update(data) checksum_str = checksum.hexdigest() resp = mc.post(f"/v2/projects/{mp.project_id()}/chunks", data, {"Content-Type": "application/octet-stream"}) diff --git a/mergin/utils.py b/mergin/utils.py index 9e7be5e..418ecd8 100644 --- a/mergin/utils.py +++ b/mergin/utils.py @@ -9,7 +9,7 @@ import tempfile from enum import Enum from typing import Optional, Type, Union, ByteString -from .common import ClientError, WorkspaceRole +from .common import ClientError def generate_checksum(file, chunk_size=4096): @@ -20,7 +20,7 @@ def generate_checksum(file, chunk_size=4096): :param chunk_size: size of chunk :return: sha1 checksum """ - checksum = hashlib.sha1() + checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ with open(file, "rb") as f: while True: chunk = f.read(chunk_size) @@ -306,7 +306,7 @@ def get_data_checksum(data: ByteString) -> str: :param data: data to calculate checksum :return: sha1 checksum """ - checksum = hashlib.sha1() + checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ checksum.update(data) return checksum.hexdigest() From a368b50b71037c55d57be8cdbc1babc78dd8a9e8 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 19 Feb 2026 08:39:31 +0100 Subject: [PATCH 20/32] explain nosec --- mergin/test/test_client.py | 4 ++-- mergin/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index e85b198..3a95edf 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -1389,7 +1389,7 @@ def _get_table_row_count(db_file, table): try: con_verify = sqlite3.connect(db_file) cursor_verify = con_verify.cursor() - cursor_verify.execute("select count(*) from {};".format(table)) # nosec B608 + cursor_verify.execute("select count(*) from {};".format(table)) # nosec B608 - internal test helper, not using user input return cursor_verify.fetchone()[0] finally: cursor_verify.close() @@ -3085,7 +3085,7 @@ def test_uploaded_chunks_cache(mc): with open(file, "rb") as file_handle: data = file_handle.read() - checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ + checksum = hashlib.sha1() # nosec B324 - usedforsecurity=False flag is compatible with python 3.9+ checksum.update(data) checksum_str = checksum.hexdigest() resp = mc.post(f"/v2/projects/{mp.project_id()}/chunks", data, {"Content-Type": "application/octet-stream"}) diff --git a/mergin/utils.py b/mergin/utils.py index 418ecd8..91796f3 100644 --- a/mergin/utils.py +++ b/mergin/utils.py @@ -20,7 +20,7 @@ def generate_checksum(file, chunk_size=4096): :param chunk_size: size of chunk :return: sha1 checksum """ - checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ + checksum = hashlib.sha1() # nosec B324 - usedforsecurity=False flag is compatible with python 3.9+ with open(file, "rb") as f: while True: chunk = f.read(chunk_size) @@ -306,7 +306,7 @@ def get_data_checksum(data: ByteString) -> str: :param data: data to calculate checksum :return: sha1 checksum """ - checksum = hashlib.sha1() # nosec B324 # usedforsecurity=False flag is compatible with python 3.9+ + checksum = hashlib.sha1() # nosec B324 - usedforsecurity=False flag is compatible with python 3.9+ checksum.update(data) return checksum.hexdigest() From 2cab67a25b079a98fe03da8d2eed759c11e12e7f Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 19 Feb 2026 08:40:55 +0100 Subject: [PATCH 21/32] black --- mergin/test/test_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 3a95edf..799ebaf 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -1389,7 +1389,9 @@ def _get_table_row_count(db_file, table): try: con_verify = sqlite3.connect(db_file) cursor_verify = con_verify.cursor() - cursor_verify.execute("select count(*) from {};".format(table)) # nosec B608 - internal test helper, not using user input + cursor_verify.execute( + "select count(*) from {};".format(table) + ) # nosec B608 - internal test helper, not using user input return cursor_verify.fetchone()[0] finally: cursor_verify.close() From c2381312e20e584f522ce381cdd1f335ff6c0c1a Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 19 Feb 2026 09:31:21 +0100 Subject: [PATCH 22/32] Checks don't run on tests --- mergin/test/test_client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 799ebaf..a19745a 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -1389,9 +1389,7 @@ def _get_table_row_count(db_file, table): try: con_verify = sqlite3.connect(db_file) cursor_verify = con_verify.cursor() - cursor_verify.execute( - "select count(*) from {};".format(table) - ) # nosec B608 - internal test helper, not using user input + cursor_verify.execute("select count(*) from {};".format(table)) return cursor_verify.fetchone()[0] finally: cursor_verify.close() @@ -3087,7 +3085,7 @@ def test_uploaded_chunks_cache(mc): with open(file, "rb") as file_handle: data = file_handle.read() - checksum = hashlib.sha1() # nosec B324 - usedforsecurity=False flag is compatible with python 3.9+ + checksum = hashlib.sha1() checksum.update(data) checksum_str = checksum.hexdigest() resp = mc.post(f"/v2/projects/{mp.project_id()}/chunks", data, {"Content-Type": "application/octet-stream"}) From c5574e81c3cb6bd6afebc2ff190c95407a0a79d7 Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 20 Feb 2026 15:13:49 +0100 Subject: [PATCH 23/32] Refactor mc fixtures and use secrets for secure password generation --- mergin/test/test_client.py | 83 +++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 2b978de..6e668d3 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -11,6 +11,7 @@ import pytz import sqlite3 import glob +import secrets from unittest.mock import patch, Mock from unittest.mock import patch, Mock @@ -55,6 +56,8 @@ USER_PWD = os.environ.get("TEST_API_PASSWORD") API_USER2 = os.environ.get("TEST_API_USERNAME2") USER_PWD2 = os.environ.get("TEST_API_PASSWORD2") +PASSWORD_DEFAULT = PASSWORD_DEFAULT = secrets.token_urlsafe(10) +DEFAULT_OVERRIDES = {"projects": 100, "api_allowed": True} TMP_DIR = tempfile.gettempdir() TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") CHANGED_SCHEMA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "modified_schema") @@ -62,18 +65,20 @@ json_headers = {"Content-Type": "application/json"} -def get_default_overrides(): - return {"projects": 100, "api_allowed": True} - - def get_limit_overrides(storage: int): return {"storage": storage, "projects": 2, "api_allowed": True} -def teardown(mc: MerginClient, client_workspace_id): +def reset_workspace_limits(mc: MerginClient, client_workspace_id): + """ + Resets workspace storage and project limits to default values after testing. + + This reset applies specifically to the pre-created test user's workspace. + It is not intended for use with dynamically generated test users. + """ mc.patch( f"/v1/tests/workspaces/{client_workspace_id}", - {"limits_override": get_default_overrides()}, + {"limits_override": DEFAULT_OVERRIDES}, {"Content-Type": "application/json"}, ) @@ -82,21 +87,18 @@ def create_project_path(name, mc): return mc.username() + "/" + name -@pytest.fixture(scope="function") -def mc(): - assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - - if API_USER != "" and USER_PWD != "": # if API_USER and USER_PWD is provided we log in user and use him for tests - user = API_USER - password = USER_PWD +def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): + if api_user != "" and user_pwd != "": # if API_USER and USER_PWD is provided we log in user and use him for tests + user = api_user + password = user_pwd client = create_client(user, password) - yield client + return client else: # if user is not provided we create one # user emial is generated with random - user = f"apitest_{create_random_suffix()}@example.com" - password = "testpass123" + user = f"apitest{test_tag}_{create_random_suffix()}@example.com" + password = PASSWORD_DEFAULT anon_client = MerginClient(SERVER_URL) anon_client.post( @@ -111,39 +113,30 @@ def mc(): create_workspace_for_client(client) # we yield client - yield client - - # afterwards we disable client and workspace - delete_test_user_and_workspace(client) + return client @pytest.fixture(scope="function") -def mc2(): +def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - if API_USER2 != "" and USER_PWD2 != "": - user = API_USER2 - password = USER_PWD2 + client = create_mc_client_and_workspace(API_USER, USER_PWD) - client = create_client(user, password) - yield client - else: - user = f"apitest2_{create_random_suffix()}@example.com" - password = "testpass123" + yield client - anon_client = MerginClient(SERVER_URL) - anon_client.post( - "/v1/tests/users", - {"email": user, "password": password}, - json_headers, - validate_auth=False, - ) + if API_USER == "" or USER_PWD == "": + delete_test_user_and_workspace(client) - client = create_client(user, password) - create_workspace_for_client(client) - yield client +@pytest.fixture(scope="function") +def mc2(): + assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" + + client = create_mc_client_and_workspace(API_USER2, USER_PWD2, test_tag="2") + + yield client + if API_USER2 == "" or USER_PWD2 == "": delete_test_user_and_workspace(client) @@ -161,12 +154,12 @@ def create_user_in_workspace(workspace_id: int, mc: MerginClient, role: Workspac client = mc.create_user( email=f"apitest_userInWorkspace{create_random_suffix()}@example.com", - password="Testpass123", + password=PASSWORD_DEFAULT, workspace_id=workspace_id, workspace_role=role, ) - return MerginClient(url=SERVER_URL, login=client["email"], password="Testpass123") + return MerginClient(url=SERVER_URL, login=client["email"], password=PASSWORD_DEFAULT) def create_random_suffix(): @@ -987,7 +980,7 @@ def test_available_workspace_storage(mc: MerginClient): # remove dummy big file from a disk remove_folders([project_dir]) - teardown(mc, client_workspace_id) + reset_workspace_limits(mc, client_workspace_id) def test_available_storage_validation2(mc, mc2): @@ -1575,7 +1568,7 @@ def test_push_gpkg_schema_change(mc): # at this point we still have an open sqlite connection to the GPKG, so checkpointing will not work correctly) mc.push_project(project_dir) - subprocess.run(["sleep", "15"]) + subprocess.run(["sleep", "5"]) # WITH TWO SQLITE copies: fails here (sqlite3.OperationalError: disk I/O error) + in geodiff log: SQLITE3: (283)recovered N frames from WAL file _check_test_table(test_gpkg) @@ -2885,7 +2878,7 @@ def test_error_projects_limit_hit(mc: MerginClient): assert e.value.http_method == "POST" assert e.value.url == f"{mc.url}v1/project/{mc.username()}" - teardown(mc, client_workspace_id) + reset_workspace_limits(mc, client_workspace_id) def test_workspace_requests(mc: MerginClient): @@ -3131,7 +3124,7 @@ def test_validate_auth(mc: MerginClient): # ----- Client with token and username/password ----- # create a client with valid auth token based on other MerginClient instance with username/password that allows relogin if the token is expired mc_auth_token_login = MerginClient( - SERVER_URL, auth_token=mc._auth_session["token"], login=mc.username(), password="testpass123" + SERVER_URL, auth_token=mc._auth_session["token"], login=mc.username(), password=PASSWORD_DEFAULT ) # this should pass and not raise an error From 0a36f3e7c4985725da767bdffa6bad3b7017af2d Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 20 Feb 2026 15:26:21 +0100 Subject: [PATCH 24/32] typo --- mergin/test/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 6e668d3..8d021a1 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -96,7 +96,7 @@ def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): return client else: # if user is not provided we create one - # user emial is generated with random + # user email is generated with random user = f"apitest{test_tag}_{create_random_suffix()}@example.com" password = PASSWORD_DEFAULT From 8fae848fe978f59f5ffd63d9143a8ad1dc6f7568 Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 20 Feb 2026 16:01:06 +0100 Subject: [PATCH 25/32] changed API_USER and USER_PWD to accept None --- mergin/test/test_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 8d021a1..a241282 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -88,7 +88,7 @@ def create_project_path(name, mc): def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): - if api_user != "" and user_pwd != "": # if API_USER and USER_PWD is provided we log in user and use him for tests + if api_user and user_pwd : # if API_USER and USER_PWD is provided we log in user and use him for tests user = api_user password = user_pwd @@ -120,11 +120,12 @@ def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" + client = create_mc_client_and_workspace(API_USER, USER_PWD) yield client - if API_USER == "" or USER_PWD == "": + if not (API_USER and USER_PWD): delete_test_user_and_workspace(client) @@ -136,7 +137,7 @@ def mc2(): yield client - if API_USER2 == "" or USER_PWD2 == "": + if not (API_USER2 and USER_PWD2): delete_test_user_and_workspace(client) From e14a529cccf29bc269f148e004666ab6e740ab27 Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 20 Feb 2026 16:19:08 +0100 Subject: [PATCH 26/32] not strong enough password --- mergin/test/test_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index a241282..ff940a0 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -56,7 +56,7 @@ USER_PWD = os.environ.get("TEST_API_PASSWORD") API_USER2 = os.environ.get("TEST_API_USERNAME2") USER_PWD2 = os.environ.get("TEST_API_PASSWORD2") -PASSWORD_DEFAULT = PASSWORD_DEFAULT = secrets.token_urlsafe(10) +PASSWORD_DEFAULT = PASSWORD_DEFAULT = secrets.token_urlsafe(20) DEFAULT_OVERRIDES = {"projects": 100, "api_allowed": True} TMP_DIR = tempfile.gettempdir() TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") @@ -88,7 +88,7 @@ def create_project_path(name, mc): def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): - if api_user and user_pwd : # if API_USER and USER_PWD is provided we log in user and use him for tests + if api_user and user_pwd: # if API_USER and USER_PWD is provided we log in user and use him for tests user = api_user password = user_pwd @@ -120,7 +120,6 @@ def create_mc_client_and_workspace(api_user, user_pwd, test_tag=""): def mc(): assert SERVER_URL and SERVER_URL.rstrip("/") != "https://app.merginmaps.com" - client = create_mc_client_and_workspace(API_USER, USER_PWD) yield client From 59c0fd633540c4b5f6e597622d08be8d285eadcb Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 20 Feb 2026 16:41:26 +0100 Subject: [PATCH 27/32] Fix double assignment of PASSWORD_DEFAULT --- mergin/test/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index ff940a0..2a0a2f5 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -56,7 +56,7 @@ USER_PWD = os.environ.get("TEST_API_PASSWORD") API_USER2 = os.environ.get("TEST_API_USERNAME2") USER_PWD2 = os.environ.get("TEST_API_PASSWORD2") -PASSWORD_DEFAULT = PASSWORD_DEFAULT = secrets.token_urlsafe(20) +PASSWORD_DEFAULT = secrets.token_urlsafe(20) DEFAULT_OVERRIDES = {"projects": 100, "api_allowed": True} TMP_DIR = tempfile.gettempdir() TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") From 661f72c1c95951303151e17b7544355515f0d4ca Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Tue, 24 Feb 2026 15:26:40 +0100 Subject: [PATCH 28/32] Bump version 0.12.3 --- mergin/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mergin/version.py b/mergin/version.py index 0e95ca8..2980492 100644 --- a/mergin/version.py +++ b/mergin/version.py @@ -1,5 +1,5 @@ # The version is also stored in ../setup.py -__version__ = "0.12.2" +__version__ = "0.12.3" # There seems to be no single nice way to keep version info just in one place: # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/setup.py b/setup.py index 44ec72e..8482e5d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.12.2", + version="0.12.3", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.", From d20d130c6ccdfc8b7561dfa4fad8c7935a33cbdb Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Wed, 25 Feb 2026 08:45:14 +0100 Subject: [PATCH 29/32] reduce docstring entropy --- mergin/merginproject.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mergin/merginproject.py b/mergin/merginproject.py index ffd10dd..b69d81c 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -330,10 +330,10 @@ def compare_file_sets(self, origin, current): :Example: - >>> origin = [{'checksum': '08b0e8caddafe74bf5c11a45f65cedf974210fed', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}] # pragma: allowlist secret - >>> current = [{'checksum': 'c9a4fd2afd513a97aba19d450396a4c9df8b2ba4', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}] # pragma: allowlist secret + >>> origin = [{'checksum': '1111111111111111111111111111111111111111', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}] + >>> current = [{'checksum': '2222222222222222222222222222222222222222', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}] >>> self.compare_file_sets(origin, current) - {"added": [{'checksum': 'c9a4fd2afd513a97aba19d450396a4c9df8b2ba4', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}], "removed": [[{'checksum': '08b0e8caddafe74bf5c11a45f65cedf974210fed', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}]], "renamed": [], "updated": []} + {"added": [{'checksum': '2222222222222222222222222222222222222222', 'path': 'test.qgs', 'size': 31980, 'mtime': '2019-08-26T11:09:30.051221+02:00'}], "removed": [[{'checksum': '1111111111111111111111111111111111111111', 'path': 'base.gpkg', 'size': 2793, 'mtime': '2019-08-26T11:08:34.051221+02:00'}]], "renamed": [], "updated": []} :param origin: origin set of files metadata :type origin: list[dict] From f98e22e1e9d65315f8d0041b5c5a92c7912ae7e8 Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 6 Mar 2026 11:22:32 +0100 Subject: [PATCH 30/32] ci: security check for py-api client --- .github/workflows/security_check.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/security_check.yml diff --git a/.github/workflows/security_check.yml b/.github/workflows/security_check.yml new file mode 100644 index 0000000..8971687 --- /dev/null +++ b/.github/workflows/security_check.yml @@ -0,0 +1,40 @@ +name: Python-api QA (Security & Style) + +# Trigger the workflow on every push +on: [push] + +jobs: + quality-assurance: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + # Upgrade pip and install security/linting tools + python -m pip install --upgrade pip + pip install bandit detect-secrets flake8 flake8-json ruff + + - name: Run Bandit (Security Scan) + # Scan the mergin folder for vulnerabilities, excluding the test directory + run: bandit -r ./mergin/ -ll --exclude ./mergin/test + + - name: Run Detect Secrets + # Scan the plugin directory for hardcoded secrets/credentials + run: detect-secrets scan ./mergin/ --all-files + + - name: Run Ruff (Linting) + # Excluding mergin/test + run: ruff check ./mergin/ --line-length 120 --exclude mergin/test + + - name: Run Flake8 (Style Check) + # Style enforcement using MerginMaps standards + # Ignoring E501 (line length) and W503 (operator line breaks) + run: | + flake8 ./mergin/ --max-line-length=120 --ignore=E501,W503 --exclude=test \ No newline at end of file From 0be79fa166cb8f7923aac351123cd1a58436183d Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 6 Mar 2026 12:27:49 +0100 Subject: [PATCH 31/32] CI: removed ruff --- .github/workflows/security_check.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/security_check.yml b/.github/workflows/security_check.yml index 8971687..d9bdb29 100644 --- a/.github/workflows/security_check.yml +++ b/.github/workflows/security_check.yml @@ -19,7 +19,7 @@ jobs: run: | # Upgrade pip and install security/linting tools python -m pip install --upgrade pip - pip install bandit detect-secrets flake8 flake8-json ruff + pip install bandit detect-secrets flake8 flake8-json - name: Run Bandit (Security Scan) # Scan the mergin folder for vulnerabilities, excluding the test directory @@ -29,10 +29,6 @@ jobs: # Scan the plugin directory for hardcoded secrets/credentials run: detect-secrets scan ./mergin/ --all-files - - name: Run Ruff (Linting) - # Excluding mergin/test - run: ruff check ./mergin/ --line-length 120 --exclude mergin/test - - name: Run Flake8 (Style Check) # Style enforcement using MerginMaps standards # Ignoring E501 (line length) and W503 (operator line breaks) From 948858a423de9feebc603b1a9ab646482e8ff8f9 Mon Sep 17 00:00:00 2001 From: DanChov Date: Fri, 6 Mar 2026 13:07:12 +0100 Subject: [PATCH 32/32] CI: temporarily disable flake8 checks --- .github/workflows/security_check.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/security_check.yml b/.github/workflows/security_check.yml index d9bdb29..679b599 100644 --- a/.github/workflows/security_check.yml +++ b/.github/workflows/security_check.yml @@ -19,7 +19,13 @@ jobs: run: | # Upgrade pip and install security/linting tools python -m pip install --upgrade pip - pip install bandit detect-secrets flake8 flake8-json + pip install bandit detect-secrets + + # - name: Install dependencies + # run: | + # # Upgrade pip and install security/linting tools + # python -m pip install --upgrade pip + # pip install bandit detect-secrets flake8 flake8-json - name: Run Bandit (Security Scan) # Scan the mergin folder for vulnerabilities, excluding the test directory @@ -29,8 +35,8 @@ jobs: # Scan the plugin directory for hardcoded secrets/credentials run: detect-secrets scan ./mergin/ --all-files - - name: Run Flake8 (Style Check) - # Style enforcement using MerginMaps standards - # Ignoring E501 (line length) and W503 (operator line breaks) - run: | - flake8 ./mergin/ --max-line-length=120 --ignore=E501,W503 --exclude=test \ No newline at end of file + # - name: Run Flake8 (Style Check) + # # Style enforcement using MerginMaps standards + # # Ignoring E501 (line length) and W503 (operator line breaks) + # run: | + # flake8 ./mergin/ --max-line-length=120 --ignore=E501,W503 --exclude=test \ No newline at end of file