From 5218ed6cd4539347199d415927c72c2d6b9ba9bc Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 27 Nov 2024 21:58:12 +0100 Subject: [PATCH 01/22] Bump version to 3.8 (#1526) --- conf/release.Dockerfile | 2 +- sonar/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/release.Dockerfile b/conf/release.Dockerfile index 2c3c3eae5..95e35d609 100644 --- a/conf/release.Dockerfile +++ b/conf/release.Dockerfile @@ -31,7 +31,7 @@ COPY ./LICENSE . COPY ./sonar/audit sonar/audit RUN pip install --upgrade pip \ -&& pip install sonar-tools==3.7 +&& pip install sonar-tools==3.8 USER ${USERNAME} WORKDIR /home/${USERNAME} diff --git a/sonar/version.py b/sonar/version.py index ef857534a..ebe4e2fcf 100644 --- a/sonar/version.py +++ b/sonar/version.py @@ -24,5 +24,5 @@ """ -PACKAGE_VERSION = "3.7" +PACKAGE_VERSION = "3.8" MIGRATION_TOOL_VERSION = "0.5" From 54ec8e70004c1f7d0e395109fda0ea34c8862558 Mon Sep 17 00:00:00 2001 From: Gimiki Date: Fri, 13 Dec 2024 10:21:51 +0100 Subject: [PATCH 02/22] fix: application update key typo #1528 (#1529) --- sonar/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar/applications.py b/sonar/applications.py index 9defec3ac..facbfa142 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -65,7 +65,7 @@ class Application(aggr.Aggregation): c.SET_TAGS: "applications/set_tags", c.GET_TAGS: "applications/show", "CREATE_BRANCH": "applications/create_branch", - "UDPATE_BRANCH": "applications/update_branch", + "UPDATE_BRANCH": "applications/update_branch", } def __init__(self, endpoint: pf.Platform, key: str, name: str) -> None: From 44b8dbc04d4728c03ba7898b4b64e39cd660a2f0 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sat, 21 Dec 2024 10:50:05 +0100 Subject: [PATCH 03/22] Tests-for-issue-1528 (#1530) * Fix urlstring for lists * fix set branch * Add tests from app branches creation * Add Class APIs * Add main_branch * Remove useless pass * Add test changing application definition * Remove dependency on application * Move exists() from Applications * Refactoring to improve leverage of ApplicationBranch * Cleanup get_object * Formatting * get_project_branch in class * Fix bug * Simplify logging * Add docstring * Changed method name for set_branches() * Fix test * Quality pass --- sonar/app_branches.py | 53 ++++++++++++--------- sonar/applications.py | 107 +++++++++++++++++++++++++++--------------- sonar/branches.py | 6 +-- sonar/platform.py | 2 +- test/conftest.py | 15 ++++++ test/test_apps.py | 26 ++++++++++ 6 files changed, 146 insertions(+), 63 deletions(-) diff --git a/sonar/app_branches.py b/sonar/app_branches.py index 0255f24d7..2a07466c1 100644 --- a/sonar/app_branches.py +++ b/sonar/app_branches.py @@ -32,18 +32,11 @@ from sonar.components import Component -from sonar.applications import Application as App from sonar.branches import Branch from sonar import exceptions, projects, utilities import sonar.sqobject as sq +from sonar.util import constants as c -APIS = { - "search": "api/components/search_projects", - "get": "api/applications/show", - "create": "api/applications/create_branch", - "delete": "api/applications/delete_branch", - "update": "api/applications/update_branch", -} _NOT_SUPPORTED = "Applications not supported in community edition" @@ -54,8 +47,14 @@ class ApplicationBranch(Component): """ CACHE = cache.Cache() - - def __init__(self, app: App, name: str, project_branches: list[Branch], is_main: bool = False) -> None: + API = { + c.CREATE: "applications/create_branch", + c.GET: "applications/show", + c.DELETE: "applications/delete_branch", + c.UPDATE: "applications/update_branch", + } + + def __init__(self, app: object, name: str, project_branches: list[Branch], is_main: bool = False) -> None: """Don't use this directly, go through the class methods to create Objects""" super().__init__(endpoint=app.endpoint, key=f"{app.key} BRANCH {name}") self.concerned_object = app @@ -67,7 +66,7 @@ def __init__(self, app: App, name: str, project_branches: list[Branch], is_main: ApplicationBranch.CACHE.put(self) @classmethod - def get_object(cls, app: App, branch_name: str) -> ApplicationBranch: + def get_object(cls, app: object, branch_name: str) -> ApplicationBranch: """Gets an Application object from SonarQube :param Application app: Reference to the Application holding that branch @@ -90,7 +89,7 @@ def get_object(cls, app: App, branch_name: str) -> ApplicationBranch: raise exceptions.ObjectNotFound(app.key, f"Application key '{app.key}' branch '{branch_name}' not found") @classmethod - def create(cls, app: App, name: str, project_branches: list[Branch]) -> ApplicationBranch: + def create(cls, app: object, name: str, project_branches: list[Branch]) -> ApplicationBranch: """Creates an ApplicationBranch object in SonarQube :param Application app: Reference to the Application holding that branch @@ -108,14 +107,14 @@ def create(cls, app: App, name: str, project_branches: list[Branch]) -> Applicat br_name = "" if branch.is_main() else branch.name params["projectBranch"].append(br_name) try: - app.endpoint.post(APIS["create"], params=params) + app.endpoint.post(ApplicationBranch.API[c.CREATE], params=params) except (ConnectionError, RequestException) as e: utilities.handle_error(e, f"creating branch {name} of {str(app)}", catch_http_statuses=(HTTPStatus.BAD_REQUEST,)) - raise exceptions.ObjectAlreadyExists(f"app.App {app.key} branch '{name}", e.response.text) + raise exceptions.ObjectAlreadyExists(f"{str(app)} branch '{name}", e.response.text) return ApplicationBranch(app=app, name=name, project_branches=project_branches) @classmethod - def load(cls, app: App, branch_data: types.ApiPayload) -> ApplicationBranch: + def load(cls, app: object, branch_data: types.ApiPayload) -> ApplicationBranch: project_branches = [] for proj_data in branch_data["projects"]: proj = projects.Project.get_object(app.endpoint, proj_data["key"]) @@ -149,7 +148,7 @@ def delete(self) -> bool: if self.is_main(): log.warning("Can't delete main %s, simply delete the application for that", str(self)) return False - return sq.delete_object(self, APIS["delete"], self.search_params(), ApplicationBranch.CACHE) + return sq.delete_object(self, ApplicationBranch.API[c.DELETE], self.search_params(), ApplicationBranch.CACHE) def reload(self, data: types.ApiPayload) -> None: """Reloads an App Branch from JSON data coming from Sonar""" @@ -180,14 +179,16 @@ def update(self, name: str, project_branches: list[Branch]) -> bool: return False params = self.search_params() params["name"] = name + if len(project_branches) > 0: + params.update({"project": [], "projectBranch": []}) for branch in project_branches: params["project"].append(branch.concerned_object.key) br_name = "" if branch.is_main() else branch.name params["projectBranch"].append(br_name) try: - ok = self.endpoint.post(APIS["update"], params=params).ok + ok = self.post(ApplicationBranch.API[c.UPDATE], params=params).ok except (ConnectionError, RequestException) as e: - utilities.handle_error(e, f"uptdating {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) + utilities.handle_error(e, f"updating {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) ApplicationBranch.CACHE.pop(self) raise exceptions.ObjectNotFound(str(self), e.response.text) @@ -195,7 +196,7 @@ def update(self, name: str, project_branches: list[Branch]) -> bool: self._project_branches = project_branches return ok - def update_name(self, new_name: str) -> bool: + def rename(self, new_name: str) -> bool: """Updates an Application Branch name :param str name: New application branch name @@ -232,12 +233,22 @@ def url(self) -> str: return f"{self.endpoint.url}/dashboard?id={self.concerned_object.key}&branch={quote(self.name)}" -def list_from(app: App, data: types.ApiPayload) -> dict[str, ApplicationBranch]: +def exists(app: object, branch: str) -> bool: + """Returns whether an application branch exists""" + try: + ApplicationBranch.get_object(app, branch) + return True + except exceptions.ObjectNotFound: + return False + + +def list_from(app: object, data: types.ApiPayload) -> dict[str, ApplicationBranch]: """Returns a dict of application branches form the pure App JSON""" if not data or "branches" not in data: return {} branch_list = {} for br in data["branches"]: - branch_data = json.loads(app.endpoint.get(APIS["get"], params={"application": app.key, "branch": br["name"]}).text)["application"] + branch_data = json.loads(app.get(ApplicationBranch.API[c.GET], params={"application": app.key, "branch": br["name"]}).text)["application"] branch_list[branch_data["branch"]] = ApplicationBranch.load(app, branch_data) + log.debug("Returning Application branch list %s", str(list(branch_list.keys()))) return branch_list diff --git a/sonar/applications.py b/sonar/applications.py index facbfa142..c69ad515d 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -36,7 +36,7 @@ import sonar.util.constants as c from sonar.util import types, cache -from sonar import exceptions, settings, projects, branches +from sonar import exceptions, settings, projects, branches, app_branches from sonar.permissions import permissions, application_permissions import sonar.sqobject as sq import sonar.aggregations as aggr @@ -184,54 +184,70 @@ def projects(self) -> dict[str, str]: self._projects[p["key"]] = p["branch"] return self._projects - def branch_exists(self, branch_name: str) -> bool: + def branch_exists(self, branch: str) -> bool: """ :return: Whether the Application branch exists :rtype: bool """ - return branch_name in self.branches() + return app_branches.exists(self, branch) def branch_is_main(self, branch: str) -> bool: """ :return: Whether the Application branch is the main branch :rtype: bool """ - br = self.branches() - return branch in br and br[branch].is_main() + return app_branches.ApplicationBranch.get_object(self, branch).is_main() - def set_branch(self, branch_name: str, branch_data: types.ObjectJsonRepr) -> Application: + def main_branch(self) -> object: + """Returns the application main branch""" + for br in self.branches().values(): + if br.is_main(): + return br + return None + + def create_branch(self, branch_name: str, branch_definition: types.ObjectJsonRepr) -> object: + """Creates an application branch + + :param str branch_name: The Application branch to set + :param dict[str, str] branch_definition, {: , : , ...} + :raises ObjectAlreadyExists: if the branch name already exists + :raises ObjectNotFound: if one of the specified projects or project branches does not exists + """ + return app_branches.ApplicationBranch.create(app=self, name=branch_name, project_branches=self.__get_project_branches(branch_definition)) + + def delete_branch(self, branch_name: str) -> bool: + """Deletes an application branch + + :param str branch_name: The Application branch to set + :raises ObjectNotFound: if the branch name does not exist + """ + app_branches.ApplicationBranch.get_object(self, branch_name).delete() + + def update_branch(self, branch_name: str, branch_definition: types.ObjectJsonRepr) -> object: + o_app_branch = app_branches.ApplicationBranch.get_object(self, branch_name) + o_app_branch.update_project_branches(new_project_branches=self.__get_project_branches(branch_definition)) + + def set_branches(self, branch_name: str, branch_data: types.ObjectJsonRepr) -> Application: """Creates or updates an Application branch with a set of project branches :param str branch_name: The Application branch to set :param dict branch_data: in format returned by api/applications/show or {"projects": {: , ...}} - :raises ObjectNotFound: if a project key does not exist or project branch does not exists + :raises ObjectNotFound: if a project key does not exist or project branch does not exist :return: self: :rtype: Application """ - project_list, branch_list = [], [] - ok = True + log.debug("Updating application branch with %s", util.json_dump(branch_data)) + branch_definition = {} for p in branch_data.get("projects", []): - (pkey, bname) = (p["projectKey"], p["branch"]) if isinstance(p, dict) else (p, branch_data["projects"][p]) - try: - o_proj = projects.Project.get_object(self.endpoint, pkey) - if bname == settings.DEFAULT_BRANCH: - bname = o_proj.main_branch().name - if not branches.exists(self.endpoint, bname, pkey): - ok = False - log.warning("Branch '%s' of %s not found while setting application branch", bname, str(o_proj)) - else: - project_list.append(pkey) - branch_list.append(bname) - except exceptions.ObjectNotFound: - ok = False - - if len(project_list) > 0: - params = {"application": self.key, "branch": branch_name, "project": project_list, "projectBranch": branch_list} - api = Application.API["CREATE_BRANCH"] - if self.branch_exists(branch_name): - api = Application.API["UPDATE_BRANCH"] - params["name"] = params["branch"] - ok = ok and self.post(api, params=params).ok + if isinstance(p, list): + branch_definition[p["projectKey"]] = p["branch"] + else: + branch_definition[p] = branch_data["projects"][p] + try: + o = app_branches.ApplicationBranch.get_object(self, branch_name) + o.update_project_branches(new_project_branches=self.__get_project_branches(branch_definition)) + except exceptions.ObjectNotFound: + self.create_branch(branch_name=branch_name, branch_definition=branch_definition) return self def branches(self) -> dict[str, object]: @@ -239,13 +255,8 @@ def branches(self) -> dict[str, object]: :return: the list of branches of the application and their definition :rtype: dict {: } """ - from sonar.app_branches import list_from - - if self._branches is not None: - return self._branches - if not self.sq_json or "branches" not in self.sq_json: - self.refresh() - self._branches = list_from(app=self, data=self.sq_json) + self.refresh() + self._branches = app_branches.list_from(app=self, data=self.sq_json) return self._branches def delete(self) -> bool: @@ -401,6 +412,7 @@ def update(self, data: types.ObjectJsonRepr) -> None: :param dict data: """ + log.info("Updating application with %s", util.json_dump(data)) if "permissions" in data: decoded_perms = {} for ptype in permissions.PERMISSION_TYPES: @@ -412,8 +424,12 @@ def update(self, data: types.ObjectJsonRepr) -> None: # self.set_permissions(util.csv_to_list(perms)) self.add_projects(_project_list(data)) self.set_tags(util.csv_to_list(data.get("tags", []))) + main_branch = self.main_branch() + for name, branch_data in data.get("branches", {}).items(): + if branch_data.get("isMain", False): + main_branch.rename(name) for name, branch_data in data.get("branches", {}).items(): - self.set_branch(name, branch_data) + self.set_branches(name, branch_data) def api_params(self, op: str = c.GET) -> types.ApiParams: ops = {c.GET: {"application": self.key}, c.SET_TAGS: {"application": self.key}, c.GET_TAGS: {"application": self.key}} @@ -423,6 +439,21 @@ def search_params(self) -> types.ApiParams: """Return params used to search/create/delete for that object""" return self.api_params(c.GET) + def __get_project_branches(self, branch_definition: types.ObjectJsonRepr): + project_branches = [] + log.debug("Getting branch definition for %s", str(branch_definition)) + list_mode = isinstance(branch_definition, list) + for proj in branch_definition: + o_proj = projects.Project.get_object(self.endpoint, proj) + if list_mode: + proj_br = o_proj.main_branch().name + else: + proj_br = branch_definition[proj] + if proj_br == util.DEFAULT: + proj_br = o_proj.main_branch().name + project_branches.append(branches.Branch.get_object(o_proj, proj_br)) + return project_branches + def _project_list(data: types.ObjectJsonRepr) -> types.KeyList: """Returns the list of project keys of an application""" diff --git a/sonar/branches.py b/sonar/branches.py index ae27c25ec..db33cb98a 100644 --- a/sonar/branches.py +++ b/sonar/branches.py @@ -92,15 +92,15 @@ def get_object(cls, concerned_object: projects.Project, branch_name: str) -> Bra if o: return o try: - data = json.loads(concerned_object.endpoint.get(APIS["list"], params={"project": concerned_object.key}).text) + data = json.loads(concerned_object.get(APIS["list"], params={"project": concerned_object.key}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"searching {str(concerned_object)} for branch '{branch_name}'", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) - raise exceptions.ObjectNotFound(concerned_object.key, f"Project '{concerned_object.key}' not found") + raise exceptions.ObjectNotFound(concerned_object.key, f"{str(concerned_object)} not found") for br in data.get("branches", []): if br["name"] == branch_name: return cls.load(concerned_object, branch_name, br) - raise exceptions.ObjectNotFound(branch_name, f"Branch '{branch_name}' of project '{concerned_object.key}' not found") + raise exceptions.ObjectNotFound(branch_name, f"Branch '{branch_name}' of {str(concerned_object)} not found") @classmethod def load(cls, concerned_object: projects.Project, branch_name: str, data: types.ApiPayload) -> Branch: diff --git a/sonar/platform.py b/sonar/platform.py index bf988cddb..7d82bac93 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -386,7 +386,7 @@ def __urlstring(self, api: str, params: types.ApiParams) -> str: if isinstance(v, datetime.date): good_params[k] = util.format_date(v) elif isinstance(v, (list, tuple, set)): - good_params[k] = ",".join(str(v)) + good_params[k] = ",".join(list(v)) return url + "?" + "&".join([f"{k}={requests.utils.quote(str(v))}" for k, v in good_params.items()]) def webhooks(self) -> dict[str, object]: diff --git a/test/conftest.py b/test/conftest.py index 44eb7dd65..705f712b2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -198,3 +198,18 @@ def get_sarif_file() -> Generator[str]: file = get_temp_filename("sarif") yield file rm(file) + + +@pytest.fixture +def get_test_application() -> Generator[applications.Application]: + """setup of tests""" + util.start_logging() + try: + o = applications.Application.get_object(endpoint=util.SQ, key=util.TEMP_KEY) + except exceptions.ObjectNotFound: + o = applications.Application.create(endpoint=util.SQ, key=util.TEMP_KEY, name=util.TEMP_KEY) + yield o + try: + o.delete() + except exceptions.ObjectNotFound: + pass diff --git a/test/test_apps.py b/test/test_apps.py index 7a4c085a2..96ab90f2a 100644 --- a/test/test_apps.py +++ b/test/test_apps.py @@ -22,6 +22,7 @@ """ applications tests """ import datetime +from collections.abc import Generator import pytest import utilities as util @@ -214,3 +215,28 @@ def test_set_tags(get_test_app: callable) -> None: def test_audit_disabled() -> None: """test_audit_disabled""" assert len(applications.audit(util.SQ, {"audit.applications": False})) == 0 + + +def test_app_branches(get_test_application: Generator[applications.Application]) -> None: + app = get_test_application + definition = { + "branches": { + "Other Branch": {"projects": {"TESTSYNC": "some-branch", "demo:jcl": "main", "training:security": "main"}}, + "BRANCH foo": {"projects": {"TESTSYNC": "some-branch", "demo:jcl": "main", "training:security": "main"}, "isMain": True}, + } + } + app.update(definition) + br = app.branches() + assert set(br.keys()) == {"BRANCH foo", "Other Branch"} + assert app.main_branch().name == "BRANCH foo" + definition = { + "branches": { + "MiBranch": {"projects": {"TESTSYNC": "main", "demo:jcl": "main", "training:security": "main"}}, + "Master": {"projects": {"TESTSYNC": "some-branch", "demo:jcl": "main", "training:security": "main"}}, + "Main Branch": {"projects": {"TESTSYNC": "some-branch", "demo:jcl": "main", "training:security": "main"}, "isMain": True}, + } + } + app.update(definition) + br = app.branches() + assert set(br.keys()) >= {"Main Branch", "Master", "MiBranch"} + assert app.main_branch().name == "Main Branch" From 9ac65bfc44fff0d39ad384e2ebc1f23cfaa59042 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sun, 22 Dec 2024 14:14:16 +0100 Subject: [PATCH 04/22] Update_for_10-8 (#1532) * Change of ADO url * Add sphinx-autodoc-typehints dependency * Adapt to new 10.8 AI code assurance APIs * Cleaner docstring for docs * Deprecate aiCodeAssurance sonar-config setting * Merge remote-tracking branch 'origin/master' into update_for_10-8 * Quality pass * Make constant private --- requirements-to-build.txt | 1 + sonar/projects.py | 49 +++++++++++++++++++++++---------------- test/test_devops.py | 2 +- test/test_projects.py | 12 +++++----- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/requirements-to-build.txt b/requirements-to-build.txt index e5d8b3185..16ec9ee14 100644 --- a/requirements-to-build.txt +++ b/requirements-to-build.txt @@ -3,4 +3,5 @@ black wheel sphinx sphinx_rtd_theme +sphinx-autodoc-typehints twine diff --git a/sonar/projects.py b/sonar/projects.py index 95e8222dd..ec7b7eb30 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -60,7 +60,7 @@ _TREE_API = "components/tree" PRJ_QUALIFIER = "TRK" APP_QUALIFIER = "APP" - +_CONTAINS_AI_CODE = "containsAiCode" _BIND_SEP = ":::" _AUDIT_BRANCHES_PARAM = "audit.projects.branches" AUDIT_MODE_PARAM = "audit.mode" @@ -1078,8 +1078,8 @@ def export(self, export_settings: types.ConfigSettings, settings_list: dict[str, settings_dict = settings.get_bulk(endpoint=self.endpoint, component=self, settings_list=settings_list, include_not_set=False) # json_data.update({s.to_json() for s in settings_dict.values() if include_inherited or not s.inherited}) ai = self.get_ai_code_assurance() - if ai: - json_data["aiCodeAssurance"] = ai + contains_ai = ai is not None and ai != "NONE" + json_data[_CONTAINS_AI_CODE] = contains_ai for s in settings_dict.values(): if not export_settings.get("INCLUDE_INHERITED", False) and s.inherited: continue @@ -1148,9 +1148,7 @@ def set_quality_gate(self, quality_gate: str) -> bool: """Sets project quality gate :param quality_gate: quality gate name - :type quality_gate: str :return: Whether the operation was successful - :rtype: bool """ if quality_gate is None: return False @@ -1166,20 +1164,30 @@ def set_quality_gate(self, quality_gate: str) -> bool: util.handle_error(e, f"setting permissions of {str(self)}", catch_all=True) return False - def set_ai_code_assurance(self, enabled: bool) -> bool: - """Sets whether a project has AI code assurance enabled or not""" - if self.endpoint.version() >= (10, 7, 0) and self.endpoint.edition() != "community": - try: - return self.post("projects/set_ai_code_assurance", params={"project": self.key, "contains_ai_code": str(enabled).lower()}).ok - except (ConnectionError, RequestException) as e: - util.handle_error(e, f"setting AI code assurance of {str(self)}", catch_all=True) - return False + def set_contains_ai_code(self, contains_ai_code: bool) -> bool: + """Sets whether a project contains AI code + + :param contains_ai_code: Whether the project contains AI code + :return: Whether the operation succeeded + """ + if self.endpoint.version() < (10, 7, 0) or self.endpoint.edition() == "community": + return False + try: + api = "projects/set_contains_ai_code" + if self.endpoint.version() == (10, 7, 0): + api = "projects/set_ai_code_assurance" + return self.post(api, params={"project": self.key, "contains_ai_code": str(contains_ai_code).lower()}).ok + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"setting contains AI code of {str(self)}", catch_all=True) + return False - def get_ai_code_assurance(self) -> Optional[bool]: - """Returns whether project AI code assurance flag is enabled or not""" + def get_ai_code_assurance(self) -> Optional[str]: + """ + :return: The AI code assurance status of the project + """ if self.endpoint.version() >= (10, 7, 0) and self.endpoint.edition() != "community": try: - return json.loads(self.get("projects/get_ai_code_assurance", params={"project": self.key}).text)["aiCodeAssurance"] + return str(json.loads(self.get("projects/get_ai_code_assurance", params={"project": self.key}).text)["aiCodeAssurance"]).upper() except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting AI code assurance of {str(self)}", catch_all=True) return None @@ -1187,10 +1195,9 @@ def get_ai_code_assurance(self) -> Optional[bool]: def set_quality_profile(self, language: str, quality_profile: str) -> bool: """Sets project quality profile for a given language - :param str language: Language mnemonic, following SonarQube convention - :param str quality_profile: Name of the quality profile in the language + :param language: Language key, following SonarQube convention + :param quality_profile: Name of the quality profile in the language :return: Whether the operation was successful - :rtype: bool """ if not qualityprofiles.exists(endpoint=self.endpoint, language=language, name=quality_profile): log.warning("Quality profile '%s' in language '%s' does not exist, can't set it for %s", quality_profile, language, str(self)) @@ -1406,7 +1413,9 @@ def update(self, data: types.ObjectJsonRepr) -> None: settings_to_apply = { k: v for k, v in data.items() if k not in ("permissions", "tags", "links", "qualityGate", "qualityProfiles", "binding", "name") } - self.set_ai_code_assurance(data.get("aiCodeAssurance", False)) + if "aiCodeAssurance" in data: + log.warning("'aiCodeAssurance' project setting is deprecated, please use '%s' instead", _CONTAINS_AI_CODE) + self.set_contains_ai_code(data.get(_CONTAINS_AI_CODE, data.get("aiCodeAssurance", False))) # TODO: Set branch settings self.set_settings(settings_to_apply) diff --git a/test/test_devops.py b/test/test_devops.py index f075f6bde..ac0c6e59e 100644 --- a/test/test_devops.py +++ b/test/test_devops.py @@ -55,7 +55,7 @@ def test_get_object_gh_refresh() -> None: def test_get_object_ado() -> None: """test_get_object_ado""" plt = devops.get_object(endpoint=util.SQ, key=ADO_KEY) - assert plt.url == "https://dev.azure.com/okorach" + assert plt.url == "https://dev.azure.com/olivierkorach" assert str(plt) == f"devops platform '{ADO_KEY}'" diff --git a/test/test_projects.py b/test/test_projects.py index 79eed8cba..2eb1c25c5 100644 --- a/test/test_projects.py +++ b/test/test_projects.py @@ -197,14 +197,14 @@ def test_set_quality_gate(get_test_project: callable) -> None: def test_ai_code_assurance(get_test_project: callable) -> None: """test_set_ai_code_assurance""" proj = get_test_project - assert proj.set_ai_code_assurance(True) - assert proj.get_ai_code_assurance() is True - assert proj.set_ai_code_assurance(False) - assert proj.get_ai_code_assurance() is False + assert proj.set_contains_ai_code(True) + assert proj.get_ai_code_assurance() in ("CONTAINS_AI_CODE", "AI_CODE_ASSURED") + assert proj.set_contains_ai_code(False) + assert proj.get_ai_code_assurance() == "NONE" proj.key = util.NON_EXISTING_KEY - assert not proj.set_ai_code_assurance(True) + assert not proj.set_contains_ai_code(True) assert proj.get_ai_code_assurance() is None - assert not proj.set_ai_code_assurance(False) + assert not proj.set_contains_ai_code(False) assert proj.get_ai_code_assurance() is None From 224a0e4f098eea2c8d6fb2eda96b101d233c051e Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sun, 22 Dec 2024 14:21:11 +0100 Subject: [PATCH 05/22] Fixes #1531 (#1534) --- conf/run_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/run_tests.sh b/conf/run_tests.sh index 96cf21bbc..416e14c75 100755 --- a/conf/run_tests.sh +++ b/conf/run_tests.sh @@ -29,6 +29,6 @@ utReport="$buildDir/xunit-results.xml" echo "Running tests" export SONAR_HOST_URL=${1:-${SONAR_HOST_URL}} -coverage run --source=$ROOTDIR -m pytest $ROOTDIR/test/ --junit-xml="$utReport" +coverage run --branch --source=$ROOTDIR -m pytest $ROOTDIR/test/ --junit-xml="$utReport" coverage xml -o $coverageReport From 3dd90f8e688953fcbbf25592f0ff3d691fe21015 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sun, 22 Dec 2024 16:53:38 +0100 Subject: [PATCH 06/22] Generalize class APIs (#1535) * SEARCH API * CREATE API * DELETE API * Class API pattern * Class API pattern * Class API SEARCH * Class CREATE API * Class UPDATE API * Remove unused symbols * Formatting * Remove useless imports * Class API * Formatting * Class API * Class API * Class API * Order as CRUDL * Class API * Class API * Class API * Class API * Class API * Use SET/GET_TAGS * Use SET/GET_TAGS * Remove unused constant * Class API * Class API * Class API * Class API * Remove SEARCH_API symbol * Remove SEARCH_API * Formatting * Fixes * Review docstrings * Quality pass * Fix * Quality pass --- sonar/app_branches.py | 2 +- sonar/applications.py | 1 - sonar/branches.py | 28 +++++++++---------- sonar/components.py | 4 +-- sonar/dce/app_nodes.py | 8 +++--- sonar/dce/search_nodes.py | 17 +++++------- sonar/devops.py | 10 +++---- sonar/groups.py | 41 +++++++++++++-------------- sonar/hotspots.py | 26 +++++++++--------- sonar/issues.py | 23 +++++++--------- sonar/organizations.py | 6 ++-- sonar/platform.py | 6 ++-- sonar/portfolios.py | 28 ++++++++----------- sonar/projects.py | 22 +++++++++------ sonar/pull_requests.py | 7 +++-- sonar/qualitygates.py | 58 +++++++++++++++++++-------------------- sonar/qualityprofiles.py | 5 +--- sonar/rules.py | 23 +++++++--------- sonar/settings.py | 36 ++++++++++++------------ sonar/sqobject.py | 6 ++-- sonar/users.py | 36 ++++++++++++------------ sonar/webhooks.py | 8 +++--- 22 files changed, 192 insertions(+), 209 deletions(-) diff --git a/sonar/app_branches.py b/sonar/app_branches.py index 2a07466c1..9f144c45b 100644 --- a/sonar/app_branches.py +++ b/sonar/app_branches.py @@ -50,8 +50,8 @@ class ApplicationBranch(Component): API = { c.CREATE: "applications/create_branch", c.GET: "applications/show", - c.DELETE: "applications/delete_branch", c.UPDATE: "applications/update_branch", + c.DELETE: "applications/delete_branch", } def __init__(self, app: object, name: str, project_branches: list[Branch], is_main: bool = False) -> None: diff --git a/sonar/applications.py b/sonar/applications.py index c69ad515d..8d7862c8d 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -54,7 +54,6 @@ class Application(aggr.Aggregation): CACHE = cache.Cache() - SEARCH_API = "components/search_projects" SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "components" API = { diff --git a/sonar/branches.py b/sonar/branches.py index db33cb98a..78443337d 100644 --- a/sonar/branches.py +++ b/sonar/branches.py @@ -29,7 +29,7 @@ import requests.utils from sonar import platform -from sonar.util import types, cache +from sonar.util import types, cache, constants as c import sonar.logging as log import sonar.sqobject as sq from sonar import components, settings, exceptions, tasks @@ -40,14 +40,6 @@ from sonar.audit.rules import get_rule, RuleId -#: APIs used for branch management -APIS = { - "list": "project_branches/list", - "rename": "project_branches/rename", - "get_new_code": "new_code_periods/list", - "delete": "project_branches/delete", -} - _UNSUPPORTED_IN_CE = "Branches not available in Community Edition" @@ -57,6 +49,12 @@ class Branch(components.Component): """ CACHE = cache.Cache() + API = { + c.LIST: "project_branches/list", + c.DELETE: "project_branches/delete", + "rename": "project_branches/rename", + "get_new_code": "new_code_periods/list", + } def __init__(self, project: projects.Project, name: str) -> None: """Don't use this, use class methods to create Branch objects @@ -92,7 +90,7 @@ def get_object(cls, concerned_object: projects.Project, branch_name: str) -> Bra if o: return o try: - data = json.loads(concerned_object.get(APIS["list"], params={"project": concerned_object.key}).text) + data = json.loads(concerned_object.get(Branch.API[c.LIST], params={"project": concerned_object.key}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"searching {str(concerned_object)} for branch '{branch_name}'", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) raise exceptions.ObjectNotFound(concerned_object.key, f"{str(concerned_object)} not found") @@ -140,7 +138,7 @@ def refresh(self) -> Branch: :rtype: Branch """ try: - data = json.loads(self.get(APIS["list"], params={"project": self.concerned_object.key}).text) + data = json.loads(self.get(Branch.API[c.LIST], params={"project": self.concerned_object.key}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"refreshing {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) Branch.CACHE.pop(self) @@ -189,7 +187,7 @@ def delete(self) -> bool: :rtype: bool """ try: - return sq.delete_object(self, APIS["delete"], {"branch": self.name, "project": self.concerned_object.key}, Branch.CACHE) + return sq.delete_object(self, Branch.API[c.DELETE], {"branch": self.name, "project": self.concerned_object.key}, Branch.CACHE) except (ConnectionError, RequestException) as e: util.handle_error(e, f"deleting {str(self)}", catch_all=True) if isinstance(e, HTTPError) and e.response.status_code == HTTPStatus.BAD_REQUEST: @@ -205,7 +203,7 @@ def new_code(self) -> str: self._new_code = settings.new_code_to_string({"inherited": True}) elif self._new_code is None: try: - data = json.loads(self.get(api=APIS["get_new_code"], params={"project": self.concerned_object.key}).text) + data = json.loads(self.get(api=Branch.API["get_new_code"], params={"project": self.concerned_object.key}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting new code period of {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) Branch.CACHE.pop(self) @@ -267,7 +265,7 @@ def rename(self, new_name: str) -> bool: return False log.info("Renaming main branch of %s from '%s' to '%s'", str(self.concerned_object), self.name, new_name) try: - self.post(APIS["rename"], params={"project": self.concerned_object.key, "name": new_name}) + self.post(Branch.API["rename"], params={"project": self.concerned_object.key, "name": new_name}) except (ConnectionError, RequestException) as e: util.handle_error(e, f"Renaming {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND, HTTPStatus.BAD_REQUEST)) if isinstance(e, HTTPError): @@ -405,7 +403,7 @@ def get_list(project: projects.Project) -> dict[str, Branch]: raise exceptions.UnsupportedOperation(_UNSUPPORTED_IN_CE) log.debug("Reading all branches of %s", str(project)) - data = json.loads(project.endpoint.get(APIS["list"], params={"project": project.key}).text) + data = json.loads(project.endpoint.get(Branch.API[c.LIST], params={"project": project.key}).text) return {branch["name"]: Branch.load(project, branch["name"], data=branch) for branch in data.get("branches", {})} diff --git a/sonar/components.py b/sonar/components.py index 0f2956055..500b551e4 100644 --- a/sonar/components.py +++ b/sonar/components.py @@ -43,8 +43,6 @@ KEY_SEPARATOR = " " _ALT_COMPONENTS = ("project", "application", "portfolio") -SEARCH_API = "components/search" -_DETAILS_API = "components/show" class Component(sq.SqObject): @@ -52,6 +50,8 @@ class Component(sq.SqObject): Abstraction of the Sonar component concept """ + API = {c.SEARCH: "components/search"} + def __init__(self, endpoint: pf.Platform, key: str, data: types.ApiPayload = None) -> None: """Constructor""" super().__init__(endpoint=endpoint, key=key) diff --git a/sonar/dce/app_nodes.py b/sonar/dce/app_nodes.py index bc712e22c..9e424350a 100644 --- a/sonar/dce/app_nodes.py +++ b/sonar/dce/app_nodes.py @@ -49,7 +49,7 @@ def plugins(self) -> Optional[dict[str, str]]: return self.json.get("Plugins", None) def health(self) -> str: - """Returns app node health, RED by default if heLTH NOT AVAILABLE""" + """Returns app node health, RED by default if health not available""" return self.json.get("Health", dce_nodes.HEALTH_RED) def node_type(self) -> str: @@ -68,6 +68,7 @@ def version(self) -> Union[tuple[int, ...], None]: return None def edition(self) -> str: + """Returns the node edition""" return self.sif.edition() def name(self) -> str: @@ -109,11 +110,10 @@ def __audit_official(self) -> list[Problem]: def audit(sub_sif: dict[str, str], sif_object: object, audit_settings: types.ConfigSettings) -> list[Problem]: """Audits application nodes of a DCE instance - :param dict sub_sif: The JSON subsection of the SIF pertaining to the App Nodes + :param sub_sif: The JSON subsection of the SIF pertaining to the App Nodes :param Sif sif_object: The Sif object - :param ConfigSettings audit_settings: Config settings for audit + :param audit_settings: Config settings for audit :return: List of Problems - :rtype: list[Problem] """ nodes = [] problems = [] diff --git a/sonar/dce/search_nodes.py b/sonar/dce/search_nodes.py index f34a1b970..4c08504d2 100644 --- a/sonar/dce/search_nodes.py +++ b/sonar/dce/search_nodes.py @@ -23,7 +23,7 @@ """ -from typing import Union +from typing import Optional import sonar.logging as log from sonar.util import types @@ -61,7 +61,8 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: log.info("%s: Auditing...", str(self)) return self.__audit_store_size() + self.__audit_available_disk() - def max_heap(self) -> Union[int, None]: + def max_heap(self) -> Optional[int]: + """Returns the node max heap or None if not found""" if self.sif.edition() != "datacenter" and self.sif.version() < (9, 0, 0): return util.jvm_heap(self.sif.search_jvm_cmdline()) try: @@ -72,7 +73,7 @@ def max_heap(self) -> Union[int, None]: return int(float(sz.split(" ")[0]) * 1024) def __audit_store_size(self) -> list[Problem]: - """Auditing the search node store size vs heap allocated to ES""" + """Audits the search node store size vs heap allocated to ES""" log.info("%s: Auditing store size", str(self)) es_heap = self.max_heap() if es_heap is None: @@ -97,6 +98,7 @@ def __audit_store_size(self) -> list[Problem]: return es_pb def __audit_available_disk(self) -> list[Problem]: + """Audits whether the node has enough free disk space""" log.info("%s: Auditing available disk space", str(self)) try: space_avail = util.int_memory(self.json[_ES_STATE]["Disk Available"]) @@ -104,12 +106,7 @@ def __audit_available_disk(self) -> list[Problem]: log.warning("%s: disk space available not found in SIF, skipping this check", str(self)) return [] store_size = self.store_size() - log.info( - "%s: Search server available disk size of %d MB and store size is %d MB", - str(self), - space_avail, - store_size, - ) + log.info("%s: Search server available disk size of %d MB and store size is %d MB", str(self), space_avail, store_size) if space_avail < 10000: return [Problem(get_rule(RuleId.LOW_FREE_DISK_SPACE_2), self, str(self), space_avail // 1024)] elif store_size * 2 > space_avail: @@ -119,7 +116,7 @@ def __audit_available_disk(self) -> list[Problem]: def __audit_index_balance(searchnodes: list[SearchNode]) -> list[Problem]: - """Audits whether ES index is decently balanced acros search nodes""" + """Audits whether ES index is decently balanced across search nodes""" log.info("Auditing search nodes store size balance") nbr_search_nodes = len(searchnodes) for i in range(nbr_search_nodes): diff --git a/sonar/devops.py b/sonar/devops.py index be248b5ec..0d78a3cf4 100644 --- a/sonar/devops.py +++ b/sonar/devops.py @@ -27,7 +27,7 @@ from requests import RequestException import sonar.logging as log -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import platform import sonar.sqobject as sq from sonar import exceptions @@ -41,7 +41,6 @@ _CREATE_API_AZURE = "alm_settings/create_azure" _CREATE_API_BITBUCKET = "alm_settings/create_bitbucket" _CREATE_API_BBCLOUD = "alm_settings/create_bitbucketcloud" -APIS = {"list": "alm_settings/list_definitions"} _TO_BE_SET = "TO_BE_SET" _IMPORTABLE_PROPERTIES = ("key", "type", "url", "workspace", "clientId", "appId") @@ -53,6 +52,7 @@ class DevopsPlatform(sq.SqObject): """ CACHE = cache.Cache() + API = {c.LIST: "alm_settings/list_definitions"} def __init__(self, endpoint: platform.Platform, key: str, platform_type: str) -> None: """Constructor""" @@ -69,7 +69,7 @@ def read(cls, endpoint: platform.Platform, key: str) -> DevopsPlatform: o = DevopsPlatform.CACHE.get(key, endpoint.url) if o: return o - data = json.loads(endpoint.get(APIS["list"]).text) + data = json.loads(endpoint.get(DevopsPlatform.API[c.LIST]).text) for plt_type, platforms in data.items(): for p in platforms: if p["key"] == key: @@ -137,7 +137,7 @@ def refresh(self) -> bool: :return: Whether the operation succeeded :rtype: bool """ - data = json.loads(self.get(APIS["list"]).text) + data = json.loads(self.get(DevopsPlatform.API[c.LIST]).text) for alm_data in data.get(self.type, {}): if alm_data["key"] != self.key: self.sq_json = alm_data @@ -209,7 +209,7 @@ def get_list(endpoint: platform.Platform) -> dict[str, DevopsPlatform]: """ if endpoint.is_sonarcloud(): raise exceptions.UnsupportedOperation("Can't get list of DevOps platforms on SonarCloud") - data = json.loads(endpoint.get(APIS["list"]).text) + data = json.loads(endpoint.get(DevopsPlatform.API[c.LIST]).text) for alm_type in DEVOPS_PLATFORM_TYPES: for alm_data in data.get(alm_type, {}): DevopsPlatform.load(endpoint, alm_type, alm_data) diff --git a/sonar/groups.py b/sonar/groups.py index 3a2a59196..e8a9e3516 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -35,16 +35,10 @@ from sonar.audit import rules from sonar.audit.problem import Problem -from sonar.util import types, cache +from sonar.util import types, cache, constants as c SONAR_USERS = "sonar-users" -_CREATE_API = "user_groups/create" -_UPDATE_API = "user_groups/update" -ADD_USER_API = "user_groups/add_user" -REMOVE_USER_API = "user_groups/remove_user" -_UPDATE_API_V2 = "v2/authorizations/groups" - class Group(sq.SqObject): """ @@ -53,8 +47,15 @@ class Group(sq.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "user_groups/search" - SEARCH_API_V2 = "v2/authorizations/groups" + SEARCH_API_V1 = "user_groups/search" + UPDATE_API_V1 = "user_groups/update" + API = { + c.CREATE: "user_groups/create", + c.UPDATE: "v2/authorizations/groups", + c.SEARCH: "v2/authorizations/groups", + "ADD_USER": "user_groups/add_user", + "REMOVE_USER": "user_groups/remove_user", + } SEARCH_KEY_FIELD = "name" SEARCH_RETURN_FIELD = "groups" @@ -82,7 +83,7 @@ def read(cls, endpoint: pf.Platform, name: str) -> Group: o = Group.CACHE.get(name, endpoint.url) if o: return o - data = util.search_by_name(endpoint, name, Group.SEARCH_API, "groups") + data = util.search_by_name(endpoint, name, Group.get_search_api(endpoint), "groups") if data is None: raise exceptions.ObjectNotFound(name, f"Group '{name}' not found.") # SonarQube 10 compatibility: "id" field is dropped, use "name" instead @@ -103,7 +104,7 @@ def create(cls, endpoint: pf.Platform, name: str, description: str = None) -> Gr :rtype: Group or None """ log.debug("Creating group '%s'", name) - endpoint.post(_CREATE_API, params={"name": name, "description": description}) + endpoint.post(Group.API[c.SEARCH], params={"name": name, "description": description}) return cls.read(endpoint=endpoint, name=name) @classmethod @@ -119,9 +120,9 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> Group: @classmethod def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.SEARCH_API - if endpoint.version() >= (10, 4, 0): - api = cls.SEARCH_API_V2 + api = cls.API[c.SEARCH] + if endpoint.version() < (10, 4, 0): + api = cls.SEARCH_API_V1 return api def __str__(self) -> str: @@ -161,7 +162,7 @@ def add_user(self, user_login: str) -> bool: :rtype: bool """ try: - r = self.post(ADD_USER_API, params={"login": user_login, "name": self.name}) + r = self.post(Group.API["ADD_USER"], params={"login": user_login, "name": self.name}) except (ConnectionError, RequestException) as e: util.handle_error(e, "adding user to group") if isinstance(e, HTTPError): @@ -179,7 +180,7 @@ def remove_user(self, user_login: str) -> bool: :return: Whether the operation succeeded :rtype: bool """ - return self.post(REMOVE_USER_API, params={"login": user_login, "name": self.name}).ok + return self.post(Group.API["REMOVE_USER"], params={"login": user_login, "name": self.name}).ok def audit(self, audit_settings: types.ConfigSettings = None) -> list[Problem]: """Audits a group and return list of problems found @@ -226,9 +227,9 @@ def set_description(self, description: str) -> bool: log.debug("Updating %s with description = %s", str(self), description) if self.endpoint.version() >= (10, 4, 0): data = json.dumps({"description": description}) - r = self.patch(f"{_UPDATE_API_V2}/{self._id}", data=data, headers={"content-type": "application/merge-patch+json"}) + r = self.patch(f"{Group.API[c.UPDATE]}/{self._id}", data=data, headers={"content-type": "application/merge-patch+json"}) else: - r = self.post(_UPDATE_API, params={"currentName": self.key, "description": description}) + r = self.post(Group.UPDATE_API_V1, params={"currentName": self.key, "description": description}) if r.ok: self.description = description return r.ok @@ -245,9 +246,9 @@ def set_name(self, name: str) -> bool: return True log.debug("Updating %s with name = %s", str(self), name) if self.endpoint.version() >= (10, 4, 0): - r = self.patch(f"{_UPDATE_API_V2}/{self.key}", params={"name": name}) + r = self.patch(f"{Group.API[c.UPDATE]}/{self.key}", params={"name": name}) else: - r = self.post(_UPDATE_API, params={"currentName": self.key, "name": name}) + r = self.post(Group.UPDATE_API_V1, params={"currentName": self.key, "name": name}) if r.ok: Group.CACHE.pop(self) self.name = name diff --git a/sonar/hotspots.py b/sonar/hotspots.py index 3adbbf878..679cdacad 100644 --- a/sonar/hotspots.py +++ b/sonar/hotspots.py @@ -31,7 +31,7 @@ import sonar.platform as pf import sonar.utilities as util -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import syncer, users from sonar import findings, rules, changelog @@ -86,7 +86,7 @@ class Hotspot(findings.Finding): """Abstraction of the Sonar hotspot concept""" CACHE = cache.Cache() - SEARCH_API = "hotspots/search" + API = {c.GET: "hotspots/show", c.SEARCH: "hotspots/search"} MAX_PAGE_SIZE = 500 MAX_SEARCH = 10000 @@ -152,7 +152,7 @@ def refresh(self) -> bool: :return: The hotspot details :rtype: Whether ther operation succeeded """ - resp = self.get("hotspots/show", {"hotspot": self.key}) + resp = self.get(Hotspot.API[c.GET], {"hotspot": self.key}) if resp.ok: self.__details = json.loads(resp.text) return resp.ok @@ -345,15 +345,15 @@ def comments(self) -> dict[str, str]: self.refresh() self._comments = {} seq = 0 - for c in self.__details["comment"]: + for cmt in self.__details["comment"]: seq += 1 - self._comments[f"{c['createdAt']}_{seq:03d}"] = { - "date": c["createdAt"], + self._comments[f"{cmt['createdAt']}_{seq:03d}"] = { + "date": cmt["createdAt"], "event": "comment", - "value": c["markdown"], - "user": c["login"], - "userName": c["login"], - "commentKey": c["key"], + "value": cmt["markdown"], + "user": cmt["login"], + "userName": cmt["login"], + "commentKey": cmt["key"], } return self._comments @@ -405,7 +405,7 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str, while True: inline_filters["p"] = p try: - data = json.loads(endpoint.get(Hotspot.SEARCH_API, params=inline_filters, mute=(HTTPStatus.NOT_FOUND,)).text) + data = json.loads(endpoint.get(Hotspot.API[c.SEARCH], params=inline_filters, mute=(HTTPStatus.NOT_FOUND,)).text) nbr_hotspots = util.nbr_total_elements(data) except (ConnectionError, RequestException) as e: util.handle_error(e, "searching hotspots", catch_all=True) @@ -416,7 +416,7 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str, if nbr_hotspots > Hotspot.MAX_SEARCH: raise TooManyHotspotsError( nbr_hotspots, - f"{nbr_hotspots} hotpots returned by api/{Hotspot.SEARCH_API}, this is more than the max {Hotspot.MAX_SEARCH} possible", + f"{nbr_hotspots} hotpots returned by api/{Hotspot.API[c.SEARCH]}, this is more than the max {Hotspot.MAX_SEARCH} possible", ) for i in data["hotspots"]: @@ -512,6 +512,6 @@ def count(endpoint: pf.Platform, **kwargs) -> int: params = {} if not kwargs else kwargs.copy() params["ps"] = 1 params = sanitize_search_filters(endpoint, params) - nbr_hotspots = util.nbr_total_elements(json.loads(endpoint.get(Hotspot.SEARCH_API, params=params, mute=(HTTPStatus.NOT_FOUND,)).text)) + nbr_hotspots = util.nbr_total_elements(json.loads(endpoint.get(Hotspot.API[c.SEARCH], params=params, mute=(HTTPStatus.NOT_FOUND,)).text)) log.debug("Hotspot counts with filters %s returned %d hotspots", str(kwargs), nbr_hotspots) return nbr_hotspots diff --git a/sonar/issues.py b/sonar/issues.py index 570d8d88c..d187db983 100644 --- a/sonar/issues.py +++ b/sonar/issues.py @@ -42,8 +42,6 @@ from sonar import users, findings, changelog, projects, rules import sonar.utilities as util -API_SET_TYPE = "issues/set_type" - COMPONENT_FILTER_OLD = "componentKeys" COMPONENT_FILTER = "components" @@ -132,10 +130,9 @@ class Issue(findings.Finding): """ CACHE = cache.Cache() - SEARCH_API = "issues/search" MAX_PAGE_SIZE = 500 MAX_SEARCH = 10000 - API = {"SEARCH": SEARCH_API, "GET_TAGS": SEARCH_API, "SET_TAGS": "issues/set_tags"} + API = {c.SEARCH: "issues/search", c.GET_TAGS: "issues/search", c.SET_TAGS: "issues/set_tags"} def __init__(self, endpoint: pf.Platform, key: str, data: ApiPayload = None, from_export: bool = False) -> None: """Constructor""" @@ -216,7 +213,7 @@ def refresh(self) -> bool: :return: whether the refresh was successful :rtype: bool """ - resp = self.get(Issue.SEARCH_API, params={"issues": self.key, "additionalFields": "_all"}) + resp = self.get(Issue.API[c.SEARCH], params={"issues": self.key, "additionalFields": "_all"}) if resp.ok: self._load(json.loads(resp.text)["issues"][0]) return resp.ok @@ -357,7 +354,7 @@ def set_type(self, new_type: str) -> bool: """ log.debug("Changing type of issue %s from %s to %s", self.key, self.type, new_type) try: - r = self.post(API_SET_TYPE, {"issue": self.key, "type": new_type}) + r = self.post("issues/set_type", {"issue": self.key, "type": new_type}) if r.ok: self.type = new_type except (ConnectionError, requests.RequestException) as e: @@ -769,7 +766,7 @@ def search_first(endpoint: pf.Platform, **params) -> Union[Issue, None]: """ filters = pre_search_filters(endpoint=endpoint, params=params) filters["ps"] = 1 - data = json.loads(endpoint.get(Issue.SEARCH_API, params=filters).text) + data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text) if len(data) == 0: return None i = data["issues"][0] @@ -792,7 +789,7 @@ def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool = log.debug("Search filters = %s", str(filters)) issue_list = {} - data = json.loads(endpoint.get(Issue.SEARCH_API, params=filters).text) + data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text) nbr_issues = util.nbr_total_elements(data) nbr_pages = util.nbr_pages(data) log.debug("Number of issues: %d - Nbr pages: %d", nbr_issues, nbr_pages) @@ -800,7 +797,7 @@ def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool = if nbr_pages > 20 and raise_error: raise TooManyIssuesError( nbr_issues, - f"{nbr_issues} issues returned by api/{Issue.SEARCH_API}, this is more than the max {Issue.MAX_SEARCH} possible", + f"{nbr_issues} issues returned by api/{Issue.API[c.SEARCH]}, this is more than the max {Issue.MAX_SEARCH} possible", ) for i in data["issues"]: @@ -811,7 +808,7 @@ def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool = return issue_list q = Queue(maxsize=0) for page in range(2, nbr_pages + 1): - q.put((endpoint, Issue.SEARCH_API, issue_list, filters, page)) + q.put((endpoint, Issue.API[c.SEARCH], issue_list, filters, page)) for i in range(threads): log.debug("Starting issue search thread %d", i) worker = Thread(target=__search_thread, args=[q]) @@ -826,7 +823,7 @@ def _get_facets(endpoint: pf.Platform, project_key: str, facets: str = "director """Returns the facets of a search""" params.update({component_filter(endpoint): project_key, "facets": facets, "ps": Issue.MAX_PAGE_SIZE, "additionalFields": "comments"}) filters = pre_search_filters(endpoint=endpoint, params=params) - data = json.loads(endpoint.get(Issue.SEARCH_API, params=filters).text) + data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text) l = {} facets_list = util.csv_to_list(facets) for f in data["facets"]: @@ -857,7 +854,7 @@ def count(endpoint: pf.Platform, **kwargs) -> int: """Returns number of issues of a search""" filters = pre_search_filters(endpoint=endpoint, params=kwargs) filters["ps"] = 1 - nbr_issues = util.nbr_total_elements(json.loads(endpoint.get(Issue.SEARCH_API, params=filters).text)) + nbr_issues = util.nbr_total_elements(json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text)) log.debug("Count issues with filters %s returned %d issues", str(kwargs), nbr_issues) return nbr_issues @@ -875,7 +872,7 @@ def count_by_rule(endpoint: pf.Platform, **kwargs) -> dict[str, int]: for i in range(nbr_slices): params["rules"] = ",".join(ruleset[i * SLICE_SIZE : min((i + 1) * SLICE_SIZE - 1, len(ruleset))]) try: - data = json.loads(endpoint.get(Issue.SEARCH_API, params=params).text)["facets"][0]["values"] + data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=params).text)["facets"][0]["values"] for d in data: if d["val"] not in ruleset: continue diff --git a/sonar/organizations.py b/sonar/organizations.py index 09853aa48..46e7e2a13 100644 --- a/sonar/organizations.py +++ b/sonar/organizations.py @@ -31,7 +31,7 @@ import sonar.logging as log import sonar.platform as pf -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import sqobject, exceptions import sonar.utilities as util @@ -48,9 +48,9 @@ class Organization(sqobject.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "api/organizations/search" SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "organizations" + API = {c.SEARCH: "organizations/search"} def __init__(self, endpoint: pf.Platform, key: str, name: str) -> None: """Don't use this directly, go through the class methods to create Objects""" @@ -77,7 +77,7 @@ def get_object(cls, endpoint: pf.Platform, key: str) -> Organization: if o: return o try: - data = json.loads(endpoint.get(Organization.SEARCH_API, params={"organizations": key}).text) + data = json.loads(endpoint.get(Organization.API[c.SEARCH], params={"organizations": key}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting organization {key}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) raise exceptions.ObjectNotFound(key, f"Organization '{key}' not found") diff --git a/sonar/platform.py b/sonar/platform.py index 7d82bac93..b4aceeee4 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -38,7 +38,7 @@ import sonar.logging as log import sonar.utilities as util -from sonar.util import types +from sonar.util import types, constants as c from sonar import errcodes, settings, devops, version, sif, exceptions from sonar.permissions import permissions, global_permissions, permission_templates @@ -336,7 +336,7 @@ def get_settings(self, settings_list: list[str] = None) -> dict[str, any]: :rtype: dict{: , ...} """ params = util.remove_nones({"keys": util.list_to_csv(settings_list)}) - resp = self.get(settings.API_GET, params=params) + resp = self.get(settings.Setting.API[c.GET], params=params) json_s = json.loads(resp.text) platform_settings = {} for s in json_s["settings"]: @@ -583,7 +583,7 @@ def _audit_project_default_visibility(self) -> list[Problem]: ) visi = json.loads(resp.text)["organization"]["projectVisibility"] else: - resp = self.get(settings.API_GET, params={"keys": "projects.default.visibility"}) + resp = self.get(settings.Setting.API[c.GET], params={"keys": "projects.default.visibility"}) visi = json.loads(resp.text)["settings"][0]["value"] log.info("Project default visibility is '%s'", visi) if config.get_property("checkDefaultProjectVisibility") and visi != "private": diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 5fe6416f7..8d379b404 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -24,17 +24,16 @@ """ from __future__ import annotations -from queue import Queue + from typing import Optional import json -import datetime from http import HTTPStatus from threading import Lock from requests import HTTPError, RequestException import sonar.logging as log import sonar.platform as pf -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import aggregations, exceptions, settings, applications, app_branches import sonar.permissions.permissions as perms @@ -47,9 +46,6 @@ _CLASS_LOCK = Lock() -_CREATE_API = "views/create" -_GET_API = "views/show" - _PORTFOLIO_QUALIFIER = "VW" _SUBPORTFOLIO_QUALIFIER = "SVW" @@ -85,9 +81,9 @@ class Portfolio(aggregations.Aggregation): Abstraction of the Sonar portfolio concept """ - SEARCH_API = "views/search" SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "components" + API = {c.CREATE: "views/create", c.GET: "views/show", c.SEARCH: "views/search"} MAX_PAGE_SIZE = 500 MAX_SEARCH = 10000 @@ -140,7 +136,7 @@ def create(cls, endpoint: pf.Platform, key: str, name: Optional[str] = None, **k params = {"name": name, "key": key, "parent": parent_key} for p in "description", "visibility": params[p] = kwargs.get(p, None) - endpoint.post(_CREATE_API, params=params) + endpoint.post(Portfolio.API[c.CREATE], params=params) o = cls(endpoint=endpoint, name=name, key=key) if parent_key: parent_p = Portfolio.get_object(endpoint, parent_key) @@ -211,7 +207,7 @@ def refresh(self) -> None: if not self.is_toplevel(): self.root_portfolio.refresh() return - data = json.loads(self.get(_GET_API, params={"key": self.key}).text) + data = json.loads(self.get(Portfolio.API[c.GET], params={"key": self.key}).text) if not self.is_sub_portfolio(): self.reload(data) self.root_portfolio.reload_sub_portfolios() @@ -310,8 +306,8 @@ def get_components(self) -> types.ApiPayload: ).text ) comp_list = {} - for c in data["components"]: - comp_list[c["key"]] = c + for cmp in data["components"]: + comp_list[cmp["key"]] = cmp return comp_list def delete(self) -> bool: @@ -394,7 +390,7 @@ def selection_mode(self) -> dict[str, str]: """Returns a portfolio selection mode""" if self._selection_mode is None: # FIXME: If portfolio is a subportfolio you must reload with sub-JSON - self.reload(json.loads(self.get(_GET_API, params={"key": self.root_portfolio.key}).text)) + self.reload(json.loads(self.get(Portfolio.API[c.GET], params={"key": self.root_portfolio.key}).text)) return {k.lower(): v for k, v in self._selection_mode.items()} def has_project(self, key: str) -> bool: @@ -596,7 +592,7 @@ def get_project_list(self) -> list[str]: try: data = json.loads(self.get("api/measures/component_tree", params=params).text) nbr_projects = util.nbr_total_elements(data) - proj_key_list += [c["refKey"] for c in data["components"]] + proj_key_list += [comp["refKey"] for comp in data["components"]] except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting projects list of {str(self)}", catch_all=True) break @@ -657,7 +653,7 @@ def search_params(self) -> types.ApiParams: def count(endpoint: pf.Platform) -> int: """Counts number of portfolios""" - return aggregations.count(api=Portfolio.SEARCH_API, endpoint=endpoint) + return aggregations.count(api=Portfolio.API[c.SEARCH], endpoint=endpoint) def get_list(endpoint: pf.Platform, key_list: types.KeyList = None, use_cache: bool = True) -> dict[str, Portfolio]: @@ -763,12 +759,12 @@ def import_config(endpoint: pf.Platform, config_data: types.ObjectJsonRepr, key_ def search_by_name(endpoint: pf.Platform, name: str) -> types.ApiPayload: """Searches portfolio by name and, if found, returns data as JSON""" - return util.search_by_name(endpoint, name, Portfolio.SEARCH_API, "components") + return util.search_by_name(endpoint, name, Portfolio.API[c.SEARCH], "components") def search_by_key(endpoint: pf.Platform, key: str) -> types.ApiPayload: """Searches portfolio by key and, if found, returns data as JSON""" - return util.search_by_key(endpoint, key, Portfolio.SEARCH_API, "components") + return util.search_by_key(endpoint, key, Portfolio.API[c.SEARCH], "components") def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> types.ObjectJsonRepr: diff --git a/sonar/projects.py b/sonar/projects.py index ec7b7eb30..fcd1f00a2 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -55,7 +55,6 @@ _CLASS_LOCK = Lock() MAX_PAGE_SIZE = 500 -_CREATE_API = "projects/create" _NAV_API = "navigation/component" _TREE_API = "components/tree" PRJ_QUALIFIER = "TRK" @@ -127,11 +126,16 @@ class Project(components.Component): """ CACHE = cache.Cache() - SEARCH_API = "projects/search" - # SEARCH_API = "components/search_projects" - This one does not require admin permission but returns APPs too SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "components" - API = {"SET_TAGS": "project_tags/set", "GET_TAGS": "components/show"} + API = { + c.CREATE: "projects/create", + c.DELETE: "projects/delete", + c.SEARCH: "projects/search", + c.SET_TAGS: "project_tags/set", + c.GET_TAGS: "components/show", + } + # SEARCH_API = "components/search_projects" - This one does not require admin permission but returns APPs too def __init__(self, endpoint: pf.Platform, key: str) -> None: """ @@ -166,7 +170,7 @@ def get_object(cls, endpoint: pf.Platform, key: str) -> Project: if o: return o try: - data = json.loads(endpoint.get(Project.SEARCH_API, params={"projects": key}, mute=(HTTPStatus.FORBIDDEN,)).text) + data = json.loads(endpoint.get(Project.API[c.SEARCH], params={"projects": key}, mute=(HTTPStatus.FORBIDDEN,)).text) if len(data["components"]) == 0: log.error("Project key '%s' not found", key) raise exceptions.ObjectNotFound(key, f"Project key '{key}' not found") @@ -206,7 +210,7 @@ def create(cls, endpoint: pf.Platform, key: str, name: str) -> Project: :rtype: Project """ try: - endpoint.post(_CREATE_API, params={"project": key, "name": name}) + endpoint.post(Project.API[c.CREATE], params={"project": key, "name": name}) except (ConnectionError, RequestException) as e: util.handle_error(e, f"creating project '{key}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) raise exceptions.ObjectAlreadyExists(key, e.response.text) @@ -232,7 +236,7 @@ def refresh(self) -> Project: :return: self :rtype: Project """ - data = json.loads(self.get(Project.SEARCH_API, params={"projects": self.key}).text) + data = json.loads(self.get(Project.API[c.SEARCH], params={"projects": self.key}).text) if len(data["components"]) == 0: Project.CACHE.pop(self) raise exceptions.ObjectNotFound(self.key, f"{str(self)} not found") @@ -359,7 +363,7 @@ def delete(self) -> bool: """ loc = int(self.get_measure("ncloc", fallback="0")) log.info("Deleting %s, name '%s' with %d LoCs", str(self), self.name, loc) - ok = sqobject.delete_object(self, "projects/delete", {"project": self.key}, Project.CACHE) + ok = sqobject.delete_object(self, Project.API[c.DELETE], {"project": self.key}, Project.CACHE) log.info("Successfully deleted %s - %d LoCs", str(self), loc) return ok @@ -1438,7 +1442,7 @@ def count(endpoint: pf.Platform, params: types.ApiParams = None) -> int: """ new_params = {} if params is None else params.copy() new_params.update({"ps": 1, "p": 1}) - return util.nbr_total_elements(json.loads(endpoint.get(Project.SEARCH_API, params=params).text)) + return util.nbr_total_elements(json.loads(endpoint.get(Project.API[c.SEARCH], params=params).text)) def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, Project]: diff --git a/sonar/pull_requests.py b/sonar/pull_requests.py index 2b4bb2e7d..f52d637b9 100644 --- a/sonar/pull_requests.py +++ b/sonar/pull_requests.py @@ -30,7 +30,7 @@ import requests.utils import sonar.logging as log -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import components, sqobject, exceptions import sonar.utilities as util from sonar.audit.rules import get_rule, RuleId @@ -46,6 +46,7 @@ class PullRequest(components.Component): """ CACHE = cache.Cache() + API = {c.DELETE: "project_pull_requests/delete", c.LIST: "project_pull_requests/list"} def __init__(self, project: object, key: str, data: types.ApiPayload = None) -> None: """Constructor""" @@ -79,7 +80,7 @@ def last_analysis(self) -> datetime: def delete(self) -> bool: """Deletes a PR and returns whether the operation succeeded""" - return sqobject.delete_object(self, "project_pull_requests/delete", self.search_params(), PullRequest.CACHE) + return sqobject.delete_object(self, PullRequest.API[c.DELETE], self.search_params(), PullRequest.CACHE) def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: age = util.age(self.last_analysis()) @@ -121,7 +122,7 @@ def get_list(project: object) -> dict[str, PullRequest]: log.debug(_UNSUPPORTED_IN_CE) raise exceptions.UnsupportedOperation(_UNSUPPORTED_IN_CE) - data = json.loads(project.get("project_pull_requests/list", params={"project": project.key}).text) + data = json.loads(project.get(PullRequest.API[c.LIST], params={"project": project.key}).text) pr_list = {} for pr in data["pullRequests"]: pr_list[pr["key"]] = get_object(pr["key"], project, pr) diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 450807ced..2e9213c71 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -24,7 +24,7 @@ """ from __future__ import annotations -from typing import Union, Optional +from typing import Union from http import HTTPStatus import json @@ -33,7 +33,7 @@ import sonar.logging as log import sonar.sqobject as sq import sonar.platform as pf -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import measures, exceptions, projects import sonar.permissions.qualitygate_permissions as permissions import sonar.utilities as util @@ -42,15 +42,6 @@ from sonar.audit.problem import Problem -#: Quality gates APIs -APIS = { - "create": "qualitygates/create", - "list": "qualitygates/list", - "rename": "qualitygates/rename", - "details": "qualitygates/show", - "get_projects": "qualitygates/search", -} - __NEW_ISSUES_SHOULD_BE_ZERO = "Any numeric threshold on new issues should be 0 or should be removed from QG conditions" GOOD_QG_CONDITIONS = { @@ -78,6 +69,13 @@ class QualityGate(sq.SqObject): Abstraction of the Sonar Quality Gate concept """ + API = { + c.CREATE: "qualitygates/create", + c.LIST: "qualitygates/list", + "rename": "qualitygates/rename", + "details": "qualitygates/show", + "get_projects": "qualitygates/search", + } CACHE = cache.Cache() def __init__(self, endpoint: pf.Platform, name: str, data: types.ApiPayload) -> None: @@ -131,7 +129,7 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> QualityGate: @classmethod def create(cls, endpoint: pf.Platform, name: str) -> Union[QualityGate, None]: """Creates an empty quality gate""" - r = endpoint.post(APIS["create"], params={"name": name}) + r = endpoint.post(QualityGate.API[c.CREATE], params={"name": name}) if not r.ok: return None return cls.get_object(endpoint, name) @@ -167,7 +165,7 @@ def projects(self) -> dict[str, projects.Project]: while page <= nb_pages: params["p"] = page try: - resp = self.get(APIS["get_projects"], params=params) + resp = self.get(QualityGate.API["get_projects"], params=params) except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting projects of {str(self)}", catch_http_errors=(HTTPStatus.NOT_FOUND,)) QualityGate.CACHE.pop(self) @@ -196,9 +194,9 @@ def conditions(self, encoded: bool = False) -> list[str]: """ if self._conditions is None: self._conditions = [] - data = json.loads(self.get(APIS["details"], params={"name": self.name}).text) - for c in data.get("conditions", []): - self._conditions.append(c) + data = json.loads(self.get(QualityGate.API["details"], params={"name": self.name}).text) + for cond in data.get("conditions", []): + self._conditions.append(cond) if encoded: return _encode_conditions(self._conditions) return self._conditions @@ -211,8 +209,8 @@ def clear_conditions(self) -> None: log.debug("Can't clear conditions of built-in %s", str(self)) else: log.debug("Clearing conditions of %s", str(self)) - for c in self.conditions(): - self.post("qualitygates/delete_condition", params={"id": c["id"]}) + for cond in self.conditions(): + self.post("qualitygates/delete_condition", params={"id": cond["id"]}) self._conditions = None def set_conditions(self, conditions_list: list[str]) -> bool: @@ -281,7 +279,7 @@ def update(self, **data) -> bool: """ if "name" in data and data["name"] != self.name: log.info("Renaming %s with %s", str(self), data["name"]) - self.post(APIS["rename"], params={"id": self.key, "name": data["name"]}) + self.post(QualityGate.API["rename"], params={"id": self.key, "name": data["name"]}) QualityGate.CACHE.pop(self) self.name = data["name"] self.key = data["name"] @@ -294,12 +292,12 @@ def update(self, **data) -> bool: def __audit_conditions(self) -> list[Problem]: problems = [] - for c in self.conditions(): - m = c["metric"] + for cond in self.conditions(): + m = cond["metric"] if m not in GOOD_QG_CONDITIONS: problems.append(Problem(get_rule(RuleId.QG_WRONG_METRIC), self, str(self), m)) continue - val = int(c["error"]) + val = int(cond["error"]) (mini, maxi, precise_msg) = GOOD_QG_CONDITIONS[m] log.info("Condition on metric '%s': Check that %d in range [%d - %d]", m, val, mini, maxi) if val < mini or val > maxi: @@ -370,7 +368,7 @@ def get_list(endpoint: pf.Platform) -> dict[str, QualityGate]: :rtype: dict {: } """ log.info("Getting quality gates") - data = json.loads(endpoint.get(APIS["list"]).text) + data = json.loads(endpoint.get(QualityGate.API[c.LIST]).text) qg_list = {} for qg in data["qualitygates"]: log.debug("Getting QG %s", util.json_dump(qg)) @@ -451,14 +449,14 @@ def exists(endpoint: pf.Platform, gate_name: str) -> bool: def _encode_conditions(conds: list[dict[str, str]]) -> list[str]: """Encode dict conditions in strings""" simple_conds = [] - for c in conds: - simple_conds.append(_encode_condition(c)) + for cond in conds: + simple_conds.append(_encode_condition(cond)) return simple_conds -def _encode_condition(c: dict[str, str]) -> str: +def _encode_condition(cond: dict[str, str]) -> str: """Encode one dict conditions in a string""" - metric, op, val = c["metric"], c["op"], c["error"] + metric, op, val = cond["metric"], cond["op"], cond["error"] if op == "GT": op = ">=" elif op == "LT": @@ -468,9 +466,9 @@ def _encode_condition(c: dict[str, str]) -> str: return f"{metric} {op} {val}" -def _decode_condition(c: str) -> tuple[str, str, str]: +def _decode_condition(cond: str) -> tuple[str, str, str]: """Decodes a string condition in a tuple metric, op, value""" - (metric, op, val) = c.strip().split(" ") + (metric, op, val) = cond.strip().split(" ") if op in (">", ">="): op = "GT" elif op in ("<", "<="): @@ -482,7 +480,7 @@ def _decode_condition(c: str) -> tuple[str, str, str]: def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, QualityGate]: """Searches quality gates matching name""" - return util.search_by_name(endpoint, name, APIS["list"], "qualitygates") + return util.search_by_name(endpoint, name, QualityGate.API[c.LIST], "qualitygates") def convert_for_yaml(original_json: types.ObjectJsonRepr) -> types.ObjectJsonRepr: diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index 44c807d9c..20600fa3d 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -42,8 +42,6 @@ from sonar.audit.rules import get_rule, RuleId from sonar.audit.problem import Problem -_DETAILS_API = "qualityprofiles/show" - _KEY_PARENT = "parent" _CHILDREN_KEY = "children" @@ -59,14 +57,13 @@ class QualityProfile(sq.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "qualityprofiles/search" SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "profiles" API = { c.CREATE: "qualityprofiles/create", - c.LIST: "qualityprofiles/search", c.GET: "qualityprofiles/search", c.DELETE: "qualityprofiles/delete", + c.LIST: "qualityprofiles/search", c.RENAME: "qualityprofiles/rename", } diff --git a/sonar/rules.py b/sonar/rules.py index 18a946f70..8c0273931 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -30,13 +30,9 @@ import sonar.logging as log import sonar.sqobject as sq -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import platform, utilities, exceptions, languages -_DETAILS_API = "rules/show" -_UPDATE_API = "rules/update" -_CREATE_API = "rules/create" - _BUG = "BUG" _VULN = "VULNERABILITY" _CODE_SMELL = "CODE_SMELL" @@ -159,10 +155,11 @@ class Rule(sq.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "rules/search" SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "rules" + API = {c.CREATE: "rules/create", c.GET: "rules/show", c.UPDATE: "rules/update", c.SEARCH: "rules/search"} + def __init__(self, endpoint: platform.Platform, key: str, data: types.ApiPayload) -> None: super().__init__(endpoint=endpoint, key=key) log.debug("Creating rule object '%s'", key) # utilities.json_dump(data)) @@ -200,7 +197,7 @@ def get_object(cls, endpoint: platform.Platform, key: str) -> Rule: if o: return o try: - r = endpoint.get(_DETAILS_API, params={"key": key}) + r = endpoint.get(Rule.API[c.GET], params={"key": key}) except (ConnectionError, RequestException) as e: utilities.handle_error(e, f"getting rule {key}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) raise exceptions.ObjectNotFound(key=key, message=f"Rule key '{key}' does not exist") @@ -214,7 +211,7 @@ def create(cls, endpoint: platform.Platform, key: str, **kwargs) -> Optional[Rul params = kwargs.copy() (_, params["customKey"]) = key.split(":") log.debug("Creating rule key '%s'", key) - if not endpoint.post(_CREATE_API, params=params).ok: + if not endpoint.post(cls.API[c.CREATE], params=params).ok: return None return cls.get_object(endpoint=endpoint, key=key) @@ -283,7 +280,7 @@ def export(self, full: bool = False) -> types.ObjectJsonRepr: def set_tags(self, tags: list[str]) -> bool: """Sets rule custom tags""" log.debug("Settings custom tags of %s to '%s' ", str(self), str(tags)) - ok = self.post(_UPDATE_API, params={"key": self.key, "tags": utilities.list_to_csv(tags)}).ok + ok = self.post(Rule.API[c.UPDATE], params={"key": self.key, "tags": utilities.list_to_csv(tags)}).ok if ok: self.tags = sorted(tags) if len(tags) > 0 else None return ok @@ -298,7 +295,7 @@ def set_description(self, description: str) -> bool: if self.endpoint.is_sonarcloud(): raise exceptions.UnsupportedOperation("Can't extend rules description on SonarCloud") log.debug("Settings custom description of %s to '%s'", str(self), description) - ok = self.post(_UPDATE_API, params={"key": self.key, "markdown_note": description}).ok + ok = self.post(Rule.API[c.UPDATE], params={"key": self.key, "markdown_note": description}).ok if ok: self.custom_desc = description if description != "" else None return ok @@ -318,7 +315,7 @@ def impacts(self) -> dict[str, str]: def get_facet(facet: str, endpoint: platform.Platform) -> dict[str, str]: """Returns a facet as a count per item in the facet""" - data = json.loads(endpoint.get(Rule.SEARCH_API, params={"ps": 1, "facets": facet}).text) + data = json.loads(endpoint.get(Rule.API[c.SEARCH], params={"ps": 1, "facets": facet}).text) return {f["val"]: f["count"] for f in data["facets"][0]["values"]} @@ -336,7 +333,7 @@ def search_keys(endpoint: platform.Platform, **params) -> list[str]: try: while new_params["p"] < nbr_pages: new_params["p"] += 1 - data = json.loads(endpoint.get(Rule.SEARCH_API, params=new_params).text) + data = json.loads(endpoint.get(Rule.API[c.SEARCH], params=new_params).text) nbr_pages = utilities.nbr_pages(data) rule_list += [r[Rule.SEARCH_KEY_FIELD] for r in data[Rule.SEARCH_RETURN_FIELD]] except (ConnectionError, RequestException) as e: @@ -346,7 +343,7 @@ def search_keys(endpoint: platform.Platform, **params) -> list[str]: def count(endpoint: platform.Platform, **params) -> int: """Count number of rules that correspond to certain filters""" - return json.loads(endpoint.get(Rule.SEARCH_API, params={**params, "ps": 1}).text)["total"] + return json.loads(endpoint.get(Rule.API[c.SEARCH], params={**params, "ps": 1}).text)["total"] def get_list(endpoint: platform.Platform, use_cache: bool = True, **params) -> dict[str, Rule]: diff --git a/sonar/settings.py b/sonar/settings.py index 4afeba09f..93825f8f5 100644 --- a/sonar/settings.py +++ b/sonar/settings.py @@ -30,7 +30,7 @@ import sonar.logging as log import sonar.platform as pf -from sonar.util import types, cache +from sonar.util import types, cache, constants as c from sonar import sqobject, exceptions import sonar.utilities as util @@ -113,13 +113,6 @@ r"^sonar\.auth\..*\.organizations$", ) -API_SET = "settings/set" -API_CREATE = API_SET -API_GET = "settings/values" -API_LIST = "settings/list_definitions" -API_NEW_CODE_GET = "new_code_periods/show" -API_NEW_CODE_SET = "new_code_periods/set" - VALID_SETTINGS = set() @@ -129,6 +122,13 @@ class Setting(sqobject.SqObject): """ CACHE = cache.Cache() + API = { + c.CREATE: "settings/set", + c.GET: "settings/values", + c.LIST: "settings/list_definitions", + "NEW_CODE_GET": "new_code_periods/show", + "NEW_CODE_SET": "new_code_periods/set", + } def __init__(self, endpoint: pf.Platform, key: str, component: object = None, data: types.ApiPayload = None) -> None: """Constructor""" @@ -152,13 +152,13 @@ def read(cls, key: str, endpoint: pf.Platform, component: object = None) -> Sett return o if key == NEW_CODE_PERIOD and not endpoint.is_sonarcloud(): params = get_component_params(component, name="project") - data = json.loads(endpoint.get(API_NEW_CODE_GET, params=params).text) + data = json.loads(endpoint.get(Setting.API["NEW_CODE_GET"], params=params).text) else: if key == NEW_CODE_PERIOD: key = "sonar.leak.period.type" params = get_component_params(component) params.update({"keys": key}) - data = json.loads(endpoint.get(API_GET, params=params, with_organization=(component is None)).text)["settings"] + data = json.loads(endpoint.get(Setting.API[c.GET], params=params, with_organization=(component is None)).text)["settings"] if not endpoint.is_sonarcloud() and len(data) > 0: data = data[0] else: @@ -169,7 +169,7 @@ def read(cls, key: str, endpoint: pf.Platform, component: object = None) -> Sett def create(cls, key: str, endpoint: pf.Platform, value: any = None, component: object = None) -> Union[Setting, None]: """Creates a setting with a custom value""" log.debug("Creating setting '%s' of component '%s' value '%s'", key, str(component), str(value)) - r = endpoint.post(API_CREATE, params={"key": key, "component": component}) + r = endpoint.post(Setting.API[c.CREATE], params={"key": key, "component": component}) if not r.ok: return None o = cls.read(key=key, endpoint=endpoint, component=component) @@ -265,7 +265,7 @@ def set(self, value: any) -> bool: params["values"] = value else: params["value"] = value - return self.post(API_SET, params=params).ok + return self.post(Setting.API[c.CREATE], params=params).ok def to_json(self, list_as_csv: bool = True) -> types.ObjectJsonRepr: val = self.value @@ -402,7 +402,7 @@ def get_bulk( params = get_component_params(component) if include_not_set: - data = json.loads(endpoint.get(API_LIST, params=params, with_organization=(component is None)).text) + data = json.loads(endpoint.get(Setting.API[c.LIST], params=params, with_organization=(component is None)).text) for s in data["definitions"]: if s["key"].endswith("coverage.reportPath") or s["key"] == "languageSpecificParameters": continue @@ -412,7 +412,7 @@ def get_bulk( if settings_list is not None: params["keys"] = util.list_to_csv(settings_list) - data = json.loads(endpoint.get(API_GET, params=params, with_organization=(component is None)).text) + data = json.loads(endpoint.get(Setting.API[c.GET], params=params, with_organization=(component is None)).text) settings_dict |= __get_settings(endpoint, data, component) # Hack since projects.default.visibility is not returned by settings/list_definitions @@ -464,10 +464,10 @@ def set_new_code_period(endpoint: pf.Platform, nc_type: str, nc_value: str, proj log.debug("Setting new code period for project '%s' branch '%s' to value '%s = %s'", str(project_key), str(branch), str(nc_type), str(nc_value)) try: if endpoint.is_sonarcloud(): - ok = endpoint.post(API_SET, params={"key": "sonar.leak.period.type", "value": nc_type, "project": project_key}).ok - ok = ok and endpoint.post(API_SET, params={"key": "sonar.leak.period", "value": nc_value, "project": project_key}).ok + ok = endpoint.post(Setting.API[c.CREATE], params={"key": "sonar.leak.period.type", "value": nc_type, "project": project_key}).ok + ok = ok and endpoint.post(Setting.API[c.CREATE], params={"key": "sonar.leak.period", "value": nc_value, "project": project_key}).ok else: - ok = endpoint.post(API_NEW_CODE_SET, params={"type": nc_type, "value": nc_value, "project": project_key, "branch": branch}).ok + ok = endpoint.post(Setting.API["NEW_CODE_SET"], params={"type": nc_type, "value": nc_value, "project": project_key, "branch": branch}).ok except (ConnectionError, RequestException) as e: util.handle_error(e, f"setting new code period of {project_key}", catch_all=True) if isinstance(e, HTTPError) and e.response.status_code == HTTPStatus.BAD_REQUEST: @@ -488,7 +488,7 @@ def get_visibility(endpoint: pf.Platform, component: object) -> str: else: if endpoint.is_sonarcloud(): raise exceptions.UnsupportedOperation("Project default visibility does not exist in SonarCloud") - data = json.loads(endpoint.get(API_GET, params={"keys": PROJECT_DEFAULT_VISIBILITY}).text) + data = json.loads(endpoint.get(Setting.API[c.GET], params={"keys": PROJECT_DEFAULT_VISIBILITY}).text) return Setting.load(key=PROJECT_DEFAULT_VISIBILITY, endpoint=endpoint, component=None, data=data["settings"][0]) diff --git a/sonar/sqobject.py b/sonar/sqobject.py index 3f7deeb27..fd5571286 100644 --- a/sonar/sqobject.py +++ b/sonar/sqobject.py @@ -40,8 +40,8 @@ class SqObject(object): """Abstraction of Sonar objects""" - SEARCH_API = None CACHE = cache.Cache + API = {c.SEARCH: None} def __init__(self, endpoint: object, key: str) -> None: self.key = key #: Object unique key (unique in its class) @@ -60,12 +60,12 @@ def __eq__(self, another: object) -> bool: @classmethod def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.SEARCH_API + api = cls.API[c.SEARCH] if endpoint.is_sonarcloud(): try: api = cls.SEARCH_API_SC except AttributeError: - api = cls.SEARCH_API + api = cls.API[c.SEARCH] return api @classmethod diff --git a/sonar/users.py b/sonar/users.py index 769ead794..cd245b77d 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -37,10 +37,7 @@ from sonar.audit.rules import get_rule, RuleId from sonar.audit.problem import Problem -CREATE_API = "users/create" -UPDATE_API = "users/update" -DEACTIVATE_API = "users/deactivate" -UPDATE_LOGIN_API = "users/update_login" + _GROUPS_API_SC = "users/groups" _GROUPS_API_V2 = "v2/authorizations/group-memberships" @@ -54,12 +51,18 @@ class User(sqobject.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "users/search" - SEARCH_API_V2 = "v2/users-management/users" + SEARCH_API_V1 = "users/search" SEARCH_KEY_FIELD = "login" SEARCH_RETURN_FIELD = "users" SEARCH_API_SC = "organizations/search_members" + API = { + c.CREATE: "users/create", + c.UPDATE: "users/update", + c.SEARCH: "v2/users-management/users", + "DEACTIVATE": "users/deactivate", + "UPDATE_LOGIN": "users/update_login", + } def __init__(self, endpoint: pf.Platform, login: str, data: types.ApiPayload) -> None: """Do not use to create users, use on of the constructor class methods""" @@ -112,7 +115,7 @@ def create(cls, endpoint: pf.Platform, login: str, name: str = None, is_local: b if is_local: params["password"] = password if password else login try: - endpoint.post(CREATE_API, params=params) + endpoint.post(User.API[c.CREATE], params=params) except (ConnectionError, RequestException) as e: util.handle_error(e, f"creating user '{login}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) raise exceptions.ObjectAlreadyExists(login, util.sonar_error(e.response)) @@ -139,11 +142,11 @@ def get_object(cls, endpoint: pf.Platform, login: str) -> User: @classmethod def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.SEARCH_API + api = cls.SEARCH_API_V1 if endpoint.is_sonarcloud(): api = cls.SEARCH_API_SC elif endpoint.version() >= (10, 4, 0): - api = cls.SEARCH_API_V2 + api = cls.API[c.SEARCH] return api def __str__(self) -> str: @@ -197,12 +200,7 @@ def refresh(self) -> User: :return: The user itself """ - if self.endpoint.is_sonarcloud(): - api = User.SEARCH_API_SC - elif self.endpoint.version() < (10, 4, 0): - api = User.SEARCH_API - else: - api = User.SEARCH_API_V2 + api = User.get_search_api(self.endpoint) data = json.loads(self.get(api, params={"q": self.login}).text) for d in data["users"]: if d["login"] == self.login: @@ -225,7 +223,7 @@ def deactivate(self) -> bool: :return: Whether the deactivation succeeded :rtype: bool """ - return self.post(DEACTIVATE_API, {"name": self.name, "login": self.login}).ok + return self.post(User.API["DEACTIVATE"], {"name": self.name, "login": self.login}).ok def tokens(self, **kwargs) -> list[tokens.UserToken]: """ @@ -257,14 +255,14 @@ def update(self, **kwargs) -> User: if self.is_local: params.update({k: kwargs[k] for k in ("name", "email") if k in kwargs and kwargs[k] != my_data[k]}) if len(params) > 1: - self.post(UPDATE_API, params=params) + self.post(User.API[c.UPDATE], params=params) if "scmAccounts" in kwargs: self.set_scm_accounts(kwargs["scmAccounts"]) if "login" in kwargs: new_login = kwargs["login"] o = User.CACHE.get(new_login, self.endpoint.url) if not o: - self.post(UPDATE_LOGIN_API, params={"login": self.login, "newLogin": new_login}) + self.post(User.API["UPDATE_LOGIN"], params={"login": self.login, "newLogin": new_login}) User.CACHE.pop(self) self.login = new_login User.CACHE.put(self) @@ -342,7 +340,7 @@ def set_scm_accounts(self, accounts_list: list[str]) -> bool: :rtype: bool """ log.debug("Setting SCM accounts of %s to '%s'", str(self), str(accounts_list)) - r = self.post(UPDATE_API, params={"login": self.login, "scmAccount": ",".join(set(accounts_list))}) + r = self.post(User.API[c.UPDATE], params={"login": self.login, "scmAccount": ",".join(set(accounts_list))}) if not r.ok: self.scm_accounts = [] return False diff --git a/sonar/webhooks.py b/sonar/webhooks.py index d32d86ea3..645eb10c4 100644 --- a/sonar/webhooks.py +++ b/sonar/webhooks.py @@ -23,7 +23,7 @@ import sonar.logging as log from sonar import platform as pf -from sonar.util import types, cache +from sonar.util import types, cache, constants as c import sonar.utilities as util import sonar.sqobject as sq @@ -38,7 +38,7 @@ class WebHook(sq.SqObject): """ CACHE = cache.Cache() - SEARCH_API = "webhooks/list" + API = {c.CREATE: "webhooks/create", c.UPDATE: "webhooks/update", c.LIST: "webhooks/list"} SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "webhooks" @@ -49,7 +49,7 @@ def __init__( super().__init__(endpoint=endpoint, key=name) if data is None: params = util.remove_nones({"name": name, "url": url, "secret": secret, "project": project}) - data = json.loads(self.post("webhooks/create", params=params).text)["webhook"] + data = json.loads(self.post(WebHook.API[c.CREATE], params=params).text)["webhook"] self.sq_json = data self.name = data["name"] #: Webhook name self.key = data["key"] #: Webhook key @@ -81,7 +81,7 @@ def update(self, **kwargs) -> None: """ params = util.remove_nones(kwargs) params.update({"webhook": self.key}) - self.post("webhooks/update", params=params) + self.post(WebHook.API[c.UPDATE], params=params) def audit(self) -> list[problem.Problem]: """ From eddda22478e174df158cf66c7e22386f34d6b052 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sun, 22 Dec 2024 20:16:27 +0100 Subject: [PATCH 07/22] Simplify delete of objects (#1536) * Generic delete() and api_params() * Generic delete() and api_params() * Generic delete() and api_params() * Generic delete() and api_params() * Clean delete * Delegate generic delete * Remove delete_object() * Add support for QualityGate delete * Formatting * Add support for delete operation * Add api_params() * Remove generate() and replace by UserToken.create() * Fix regression * Add import for Class Name recognition * Fix regression * Use api_params() instead of search_params() * Use api_params() instead of search_params() * Fix type comparison * Detect indexing problems * Fix nbr_total_elements in search objects --- sonar/app_branches.py | 9 ++++--- sonar/applications.py | 18 +++---------- sonar/branches.py | 16 ++++++------ sonar/components.py | 21 +++++++-------- sonar/measures.py | 10 +++---- sonar/portfolios.py | 29 +++++++++++---------- sonar/projects.py | 11 +++----- sonar/pull_requests.py | 9 +++---- sonar/qualitygates.py | 14 +++++++--- sonar/rules.py | 7 ++++- sonar/sqobject.py | 22 +++++----------- sonar/tokens.py | 59 ++++++++++++++++++++++++++---------------- sonar/users.py | 18 ++++++++----- sonar/utilities.py | 2 +- 14 files changed, 124 insertions(+), 121 deletions(-) diff --git a/sonar/app_branches.py b/sonar/app_branches.py index 9f144c45b..05f135971 100644 --- a/sonar/app_branches.py +++ b/sonar/app_branches.py @@ -148,7 +148,7 @@ def delete(self) -> bool: if self.is_main(): log.warning("Can't delete main %s, simply delete the application for that", str(self)) return False - return sq.delete_object(self, ApplicationBranch.API[c.DELETE], self.search_params(), ApplicationBranch.CACHE) + return super().delete() def reload(self, data: types.ApiPayload) -> None: """Reloads an App Branch from JSON data coming from Sonar""" @@ -177,7 +177,7 @@ def update(self, name: str, project_branches: list[Branch]) -> bool: """ if not name and not project_branches: return False - params = self.search_params() + params = self.api_params() params["name"] = name if len(project_branches) > 0: params.update({"project": [], "projectBranch": []}) @@ -214,9 +214,10 @@ def update_project_branches(self, new_project_branches: list[Branch]) -> bool: """ return self.update(name=self.name, project_branches=new_project_branches) - def search_params(self) -> types.ApiParams: + def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" - return {"application": self.concerned_object.key, "branch": self.name} + ops = {c.GET: {"application": self.concerned_object.key, "branch": self.name}} + return ops[op] if op in ops else ops[c.GET] def component_data(self) -> types.Obj: """Returns key data""" diff --git a/sonar/applications.py b/sonar/applications.py index 8d7862c8d..4e11c1f25 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -36,7 +36,7 @@ import sonar.util.constants as c from sonar.util import types, cache -from sonar import exceptions, settings, projects, branches, app_branches +from sonar import exceptions, projects, branches, app_branches from sonar.permissions import permissions, application_permissions import sonar.sqobject as sq import sonar.aggregations as aggr @@ -262,17 +262,11 @@ def delete(self) -> bool: """Deletes an Application and all its branches :return: Whether the delete succeeded - :rtype: bool """ - ok = True if self.branches() is not None: for branch in self.branches().values(): - if not branch.is_main: - ok = ok and branch.delete() - ok = ok and sq.delete_object(self, "applications/delete", {"application": self.key}, Application.CACHE) - if ok: - Application.CACHE.pop(self) - return ok + branch.delete() + return super().delete() def get_filtered_branches(self, filters: dict[str, str]) -> Union[None, dict[str, object]]: """Get lists of branches according to the filter""" @@ -431,13 +425,9 @@ def update(self, data: types.ObjectJsonRepr) -> None: self.set_branches(name, branch_data) def api_params(self, op: str = c.GET) -> types.ApiParams: - ops = {c.GET: {"application": self.key}, c.SET_TAGS: {"application": self.key}, c.GET_TAGS: {"application": self.key}} + ops = {c.GET: {"application": self.key}} return ops[op] if op in ops else ops[c.GET] - def search_params(self) -> types.ApiParams: - """Return params used to search/create/delete for that object""" - return self.api_params(c.GET) - def __get_project_branches(self, branch_definition: types.ObjectJsonRepr): project_branches = [] log.debug("Getting branch definition for %s", str(branch_definition)) diff --git a/sonar/branches.py b/sonar/branches.py index 78443337d..639cd6b12 100644 --- a/sonar/branches.py +++ b/sonar/branches.py @@ -52,7 +52,7 @@ class Branch(components.Component): API = { c.LIST: "project_branches/list", c.DELETE: "project_branches/delete", - "rename": "project_branches/rename", + c.RENAME: "project_branches/rename", "get_new_code": "new_code_periods/list", } @@ -138,7 +138,7 @@ def refresh(self) -> Branch: :rtype: Branch """ try: - data = json.loads(self.get(Branch.API[c.LIST], params={"project": self.concerned_object.key}).text) + data = json.loads(self.get(Branch.API[c.LIST], params=self.api_params(c.LIST)).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"refreshing {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) Branch.CACHE.pop(self) @@ -187,9 +187,8 @@ def delete(self) -> bool: :rtype: bool """ try: - return sq.delete_object(self, Branch.API[c.DELETE], {"branch": self.name, "project": self.concerned_object.key}, Branch.CACHE) + return super().delete() except (ConnectionError, RequestException) as e: - util.handle_error(e, f"deleting {str(self)}", catch_all=True) if isinstance(e, HTTPError) and e.response.status_code == HTTPStatus.BAD_REQUEST: log.warning("Can't delete %s, it's the main branch", str(self)) return False @@ -203,7 +202,7 @@ def new_code(self) -> str: self._new_code = settings.new_code_to_string({"inherited": True}) elif self._new_code is None: try: - data = json.loads(self.get(api=Branch.API["get_new_code"], params={"project": self.concerned_object.key}).text) + data = json.loads(self.get(api=Branch.API["get_new_code"], params=self.api_params(c.LIST)).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"getting new code period of {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,)) Branch.CACHE.pop(self) @@ -265,7 +264,7 @@ def rename(self, new_name: str) -> bool: return False log.info("Renaming main branch of %s from '%s' to '%s'", str(self.concerned_object), self.name, new_name) try: - self.post(Branch.API["rename"], params={"project": self.concerned_object.key, "name": new_name}) + self.post(Branch.API[c.RENAME], params={"project": self.concerned_object.key, "name": new_name}) except (ConnectionError, RequestException) as e: util.handle_error(e, f"Renaming {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND, HTTPStatus.BAD_REQUEST)) if isinstance(e, HTTPError): @@ -381,9 +380,10 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: log.error("%s while auditing %s, audit skipped", util.error_msg(e), str(self)) return [] - def search_params(self) -> types.ApiParams: + def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" - return {"project": self.concerned_object.key, "branch": self.name} + ops = {c.GET: {"project": self.concerned_object.key, "branch": self.name}, c.LIST: {"project": self.concerned_object.key}} + return ops[op] if op in ops else ops[c.GET] def last_task(self) -> Optional[tasks.Task]: """Returns the last analysis background task of a problem, or none if not found""" diff --git a/sonar/components.py b/sonar/components.py index 500b551e4..8a52621c8 100644 --- a/sonar/components.py +++ b/sonar/components.py @@ -42,8 +42,6 @@ # Character forbidden in keys that can be used to separate a key from a post fix KEY_SEPARATOR = " " -_ALT_COMPONENTS = ("project", "application", "portfolio") - class Component(sq.SqObject): """ @@ -118,7 +116,7 @@ def get_issues(self, filters: types.ApiParams = None) -> dict[str, object]: from sonar.issues import search_all log.info("Searching issues for %s with filters %s", str(self), str(filters)) - params = self.search_params() + params = self.api_params(c.GET) if filters is not None: params.update(filters) params["additionalFields"] = "comments" @@ -130,7 +128,7 @@ def count_specific_rules_issues(self, ruleset: list[str], filters: types.ApiPara """Returns the count of issues of a component for a given ruleset""" from sonar.issues import count_by_rule - params = self.search_params() + params = self.api_params(c.GET) if filters is not None: params.update(filters) params["facets"] = "rules" @@ -150,7 +148,7 @@ def get_hotspots(self, filters: types.ApiParams = None) -> dict[str, object]: from sonar.hotspots import component_filter, search log.info("Searching hotspots for %s with filters %s", str(self), str(filters)) - params = utilities.replace_keys(_ALT_COMPONENTS, component_filter(self.endpoint), self.search_params()) + params = utilities.replace_keys(measures.ALT_COMPONENTS, component_filter(self.endpoint), self.api_params(c.GET)) if filters is not None: params.update(filters) return search(endpoint=self.endpoint, filters=params) @@ -173,7 +171,7 @@ def migration_export(self, export_settings: types.ConfigSettings) -> dict[str, a tpissues = self.count_third_party_issues() inst_issues = self.count_instantiated_rules_issues() - params = self.search_params() + params = self.api_params(c.GET) json_data["issues"] = { "thirdParty": tpissues if len(tpissues) > 0 else 0, "instantiatedRules": inst_issues if len(inst_issues) > 0 else 0, @@ -215,7 +213,7 @@ def loc(self) -> int: def get_navigation_data(self) -> types.ApiPayload: """Returns a component navigation data""" - params = utilities.replace_keys(_ALT_COMPONENTS, "component", self.search_params()) + params = utilities.replace_keys(measures.ALT_COMPONENTS, "component", self.api_params(c.GET)) data = json.loads(self.get("navigation/component", params=params).text) self.sq_json.update(data) return data @@ -279,13 +277,12 @@ def get_measures_history(self, metrics_list: types.KeyList) -> dict[str, str]: def api_params(self, op: str = c.LIST) -> types.ApiParams: from sonar.issues import component_filter - ops = {c.LIST: {component_filter(self.endpoint): self.key}, c.SET_TAGS: {"issue": self.key}, c.GET_TAGS: {"issues": self.key}} + ops = { + c.GET: {"component": self.key}, + c.LIST: {component_filter(self.endpoint): self.key}, + } return ops[op] if op in ops else ops[c.LIST] - def search_params(self) -> types.ApiParams: - """Return params used to search/create/delete for that object""" - return self.api_params(c.LIST) - def component_data(self) -> dict[str, str]: """Returns key data""" return {"key": self.key, "name": self.name, "type": type(self).__name__.upper(), "branch": "", "url": self.url()} diff --git a/sonar/measures.py b/sonar/measures.py index 4d10843d3..7e90924f0 100644 --- a/sonar/measures.py +++ b/sonar/measures.py @@ -29,12 +29,12 @@ from requests import RequestException from sonar import metrics, exceptions, platform from sonar.util.types import ApiPayload, ApiParams, KeyList -from sonar.util import cache +from sonar.util import cache, constants as c import sonar.logging as log import sonar.utilities as util import sonar.sqobject as sq -_ALT_COMPONENTS = ("project", "application", "portfolio") +ALT_COMPONENTS = ("project", "application", "portfolio", "key") DATETIME_METRICS = ("last_analysis", "createdAt", "updatedAt", "creation_date", "modification_date") @@ -82,7 +82,7 @@ def refresh(self) -> any: :return: The new measure value :rtype: int or float or str """ - params = util.replace_keys(_ALT_COMPONENTS, "component", self.concerned_object.search_params()) + params = util.replace_keys(ALT_COMPONENTS, "component", self.concerned_object.api_params(c.GET)) data = json.loads(self.get(Measure.API_READ, params=params).text)["component"]["measures"] self.value = self.__converted_value(_search_value(data)) return self.value @@ -138,7 +138,7 @@ def get(concerned_object: object, metrics_list: KeyList, **kwargs) -> dict[str, :return: Dict of found measures :rtype: dict{: } """ - params = util.replace_keys(_ALT_COMPONENTS, "component", concerned_object.search_params()) + params = util.replace_keys(ALT_COMPONENTS, "component", concerned_object.api_params(c.GET)) params["metricKeys"] = util.list_to_csv(metrics_list) log.debug("Getting measures with %s", str(params)) @@ -167,7 +167,7 @@ def get_history(concerned_object: object, metrics_list: KeyList, **kwargs) -> li """ # http://localhost:9999/api/measures/search_history?component=okorach_sonar-tools&metrics=ncloc&p=1&ps=1000 - params = util.replace_keys(_ALT_COMPONENTS, "component", concerned_object.search_params()) + params = util.replace_keys(ALT_COMPONENTS, "component", concerned_object.api_params(c.GET)) params["metrics"] = util.list_to_csv(metrics_list) log.debug("Getting measures history with %s", str(params)) diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 8d379b404..28643b7c4 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -83,7 +83,14 @@ class Portfolio(aggregations.Aggregation): SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "components" - API = {c.CREATE: "views/create", c.GET: "views/show", c.SEARCH: "views/search"} + API = { + c.CREATE: "views/create", + c.GET: "views/show", + c.UPDATE: "views/update", + c.DELETE: "views/delete", + c.SEARCH: "views/search", + "REFRESH": "views/refresh", + } MAX_PAGE_SIZE = 500 MAX_SEARCH = 10000 @@ -310,11 +317,6 @@ def get_components(self) -> types.ApiPayload: comp_list[cmp["key"]] = cmp return comp_list - def delete(self) -> bool: - """Deletes a portfolio, returns whether the operation succeeded""" - log.info("Deleting %s", str(self)) - return sq.delete_object(self, "views/delete", {"key": self.key}, Portfolio.CACHE) - def _audit_empty(self, audit_settings: types.ConfigSettings) -> list[problem.Problem]: """Audits if a portfolio is empty (no projects)""" if not audit_settings.get("audit.portfolios.empty", True): @@ -508,13 +510,13 @@ def set_selection_mode(self, data: dict[str, str]) -> Portfolio: def set_description(self, desc: str) -> Portfolio: if desc: - self.post("views/update", params={"key": self.key, "name": self.name, "description": desc}) + self.post(Portfolio.API[c.UPDATE], params={"key": self.key, "name": self.name, "description": desc}) self._description = desc return self def set_name(self, name: str) -> Portfolio: if name: - self.post("views/update", params={"key": self.key, "name": name}) + self.post(Portfolio.API[c.UPDATE], params={"key": self.key, "name": name}) self.name = name return self @@ -578,8 +580,8 @@ def is_subporfolio_of(self, key: str) -> bool: def recompute(self) -> bool: """Triggers portfolio recomputation, return whether operation REQUEST succeeded""" log.debug("Recomputing %s", str(self)) - key = self.root_portfolio.key if self.root_portfolio else self.key - return self.post("views/refresh", params={"key": key}).ok + params = self.root_portfolio.api_params() if self.root_portfolio else self.api_params() + return self.post(Portfolio.API["REFRESH"], params=params).ok def get_project_list(self) -> list[str]: log.debug("Search %s projects list", str(self)) @@ -646,9 +648,10 @@ def update(self, data: dict[str, str], recurse: bool) -> None: o_subp = self.add_subportfolio(key=key, name=subp_data["name"], by_ref=False) o_subp.update(data=subp_data, recurse=True) - def search_params(self) -> types.ApiParams: + def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" - return {"portfolio": self.key} + ops = {c.GET: {"key": self.key}} + return ops[op] if op in ops else ops[c.GET] def count(endpoint: pf.Platform) -> int: @@ -807,7 +810,7 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg def recompute(endpoint: pf.Platform) -> None: """Triggers recomputation of all portfolios""" - endpoint.post("views/refresh") + endpoint.post(Portfolio.API["REFRESH"]) def _find_sub_portfolio(key: str, data: types.ApiPayload) -> types.ApiPayload: diff --git a/sonar/projects.py b/sonar/projects.py index fcd1f00a2..69e432abb 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -363,9 +363,7 @@ def delete(self) -> bool: """ loc = int(self.get_measure("ncloc", fallback="0")) log.info("Deleting %s, name '%s' with %d LoCs", str(self), self.name, loc) - ok = sqobject.delete_object(self, Project.API[c.DELETE], {"project": self.key}, Project.CACHE) - log.info("Successfully deleted %s - %d LoCs", str(self), loc) - return ok + return super().delete() def has_binding(self) -> bool: """Whether the project has a DevOps platform binding""" @@ -1424,12 +1422,9 @@ def update(self, data: types.ObjectJsonRepr) -> None: self.set_settings(settings_to_apply) def api_params(self, op: str = c.GET) -> types.ApiParams: - ops = {c.GET: {"project": self.key}, c.SET_TAGS: {"project": self.key}, c.GET_TAGS: {"project": self.key}} - return ops[op] if op in ops else ops[c.GET] - - def search_params(self) -> types.ApiParams: """Return params used to search/create/delete for that object""" - return self.api_params(c.GET) + ops = {c.GET: {"project": self.key}} + return ops[op] if op in ops else ops[c.GET] def count(endpoint: pf.Platform, params: types.ApiParams = None) -> int: diff --git a/sonar/pull_requests.py b/sonar/pull_requests.py index f52d637b9..0bbc717d4 100644 --- a/sonar/pull_requests.py +++ b/sonar/pull_requests.py @@ -78,10 +78,6 @@ def last_analysis(self) -> datetime: self._last_analysis = util.string_to_date(self.json["analysisDate"]) return self._last_analysis - def delete(self) -> bool: - """Deletes a PR and returns whether the operation succeeded""" - return sqobject.delete_object(self, PullRequest.API[c.DELETE], self.search_params(), PullRequest.CACHE) - def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: age = util.age(self.last_analysis()) if age is None: # Main branch not analyzed yet @@ -94,9 +90,10 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: log.debug("%s age is %d days", str(self), age) return problems - def search_params(self) -> types.ApiParams: + def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" - return {"project": self.concerned_object.key, "pullRequest": self.key} + ops = {c.GET: {"project": self.concerned_object.key, "pullRequest": self.key}} + return ops[op] if op in ops else ops[c.GET] def get_object(pull_request_key: str, project: object, data: types.ApiPayload = None) -> Optional[PullRequest]: diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 2e9213c71..031153325 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -71,9 +71,10 @@ class QualityGate(sq.SqObject): API = { c.CREATE: "qualitygates/create", + c.GET: "qualitygates/show", + c.DELETE: "qualitygates/destroy", c.LIST: "qualitygates/list", - "rename": "qualitygates/rename", - "details": "qualitygates/show", + c.RENAME: "qualitygates/rename", "get_projects": "qualitygates/search", } CACHE = cache.Cache() @@ -194,7 +195,7 @@ def conditions(self, encoded: bool = False) -> list[str]: """ if self._conditions is None: self._conditions = [] - data = json.loads(self.get(QualityGate.API["details"], params={"name": self.name}).text) + data = json.loads(self.get(QualityGate.API[c.GET], params=self.api_params()).text) for cond in data.get("conditions", []): self._conditions.append(cond) if encoded: @@ -279,7 +280,7 @@ def update(self, **data) -> bool: """ if "name" in data and data["name"] != self.name: log.info("Renaming %s with %s", str(self), data["name"]) - self.post(QualityGate.API["rename"], params={"id": self.key, "name": data["name"]}) + self.post(QualityGate.API[c.RENAME], params={"id": self.key, "name": data["name"]}) QualityGate.CACHE.pop(self) self.name = data["name"] self.key = data["name"] @@ -290,6 +291,11 @@ def update(self, **data) -> bool: self.set_as_default() return ok + def api_params(self, op: str = c.GET) -> types.ApiParams: + """Return params used to search/create/delete for that object""" + ops = {c.GET: {"name": self.name}} + return ops[op] if op in ops else ops[c.GET] + def __audit_conditions(self) -> list[Problem]: problems = [] for cond in self.conditions(): diff --git a/sonar/rules.py b/sonar/rules.py index 8c0273931..49bc054cc 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -158,7 +158,7 @@ class Rule(sq.SqObject): SEARCH_KEY_FIELD = "key" SEARCH_RETURN_FIELD = "rules" - API = {c.CREATE: "rules/create", c.GET: "rules/show", c.UPDATE: "rules/update", c.SEARCH: "rules/search"} + API = {c.CREATE: "rules/create", c.GET: "rules/show", c.UPDATE: "rules/update", c.DELETE: "rules/delete", c.SEARCH: "rules/search"} def __init__(self, endpoint: platform.Platform, key: str, data: types.ApiPayload) -> None: super().__init__(endpoint=endpoint, key=key) @@ -312,6 +312,11 @@ def impacts(self) -> dict[str, str]: """Returns the rule clean code attributes""" return self._impacts + def api_params(self, op: str = c.GET) -> types.ApiParams: + """Return params used to search/create/delete for that object""" + ops = {c.GET: {"key": self.key}} + return ops[op] if op in ops else ops[c.GET] + def get_facet(facet: str, endpoint: platform.Platform) -> dict[str, str]: """Returns a facet as a count per item in the facet""" diff --git a/sonar/sqobject.py b/sonar/sqobject.py index fd5571286..99e8f2716 100644 --- a/sonar/sqobject.py +++ b/sonar/sqobject.py @@ -54,7 +54,7 @@ def __hash__(self) -> int: return hash((self.key, self.endpoint.url)) def __eq__(self, another: object) -> bool: - if type(self) == type(another): + if type(self) is type(another): return hash(self) == hash(another) return NotImplemented @@ -228,7 +228,11 @@ def search_objects(endpoint: object, object_class: any, params: types.ApiParams, data = json.loads(endpoint.get(api, params=new_params).text) nb_pages = utilities.nbr_pages(data) nb_objects = max(len(data[returned_field]), utilities.nbr_total_elements(data)) - log.debug("Loading %d %ss...", nb_objects, object_class.__name__) + log.debug("Loading %d %ss page of %d elements...", nb_objects, object_class.__name__, len(data[returned_field])) + if utilities.nbr_total_elements(data) > 0 and len(data[returned_field]) == 0: + msg = f"Index on {object_class.__name__} is corrupted, please reindex before using API" + log.fatal(msg) + raise exceptions.SonarException(msg) for obj in data[returned_field]: if object_class.__name__ in ("Portfolio", "Group", "QualityProfile", "User", "Application", "Project", "Organization"): objects_list[obj[key_field]] = object_class.load(endpoint=endpoint, data=obj) @@ -248,17 +252,3 @@ def search_objects(endpoint: object, object_class: any, params: types.ApiParams, worker.start() q.join() return objects_list - - -def delete_object(object: SqObject, api: str, params: types.ApiParams, class_cache: object) -> bool: - """Deletes a Sonar object""" - try: - log.info("Deleting %s", str(object)) - r = object.post(api, params=params, mute=(HTTPStatus.NOT_FOUND,)) - class_cache.pop(object) - log.info("Successfully deleted %s", str(object)) - return r.ok - except (ConnectionError, RequestException) as e: - utilities.handle_error(e, f"deleting {str(object)}", catch_http_errors=(HTTPStatus.NOT_FOUND,)) - class_cache.pop(object) - raise exceptions.ObjectNotFound(object.key, f"{str(object)} not found for delete") diff --git a/sonar/tokens.py b/sonar/tokens.py index 58a4d71c9..b172a4f66 100644 --- a/sonar/tokens.py +++ b/sonar/tokens.py @@ -20,15 +20,21 @@ """Abstraction of the SonarQube User Token concept""" +from __future__ import annotations + from typing import Optional import json import datetime +from http import HTTPStatus +from requests import RequestException + import sonar.logging as log import sonar.sqobject as sq import sonar.platform as pf import sonar.utilities as util -from sonar.util import types, cache +from sonar import exceptions +from sonar.util import types, cache, constants as c from sonar.audit.problem import Problem from sonar.audit.rules import get_rule, RuleId @@ -39,10 +45,7 @@ class UserToken(sq.SqObject): """ CACHE = cache.Cache() - API_ROOT = "user_tokens" - API_REVOKE = API_ROOT + "/revoke" - API_SEARCH = API_ROOT + "/search" - API_GENERATE = API_ROOT + "/generate" + API = {c.CREATE: "user_tokens/generate", c.DELETE: "user_tokens/revoke", c.LIST: "user_tokens/search"} def __init__(self, endpoint: pf.Platform, login: str, json_data: types.ApiPayload, name: str = None) -> None: """Constructor""" @@ -57,24 +60,44 @@ def __init__(self, endpoint: pf.Platform, login: str, json_data: types.ApiPayloa self.token = json_data.get("token", None) log.debug("Created '%s'", str(self)) + @classmethod + def create(cls, endpoint: pf.Platform, login: str, name: str) -> UserToken: + """Creates a user token in SonarQube + + :param endpoint: Reference to the SonarQube platform + :param login: User for which the token must be created + :param name: Token name + :return: The UserToken + """ + try: + data = json.loads(endpoint.post(UserToken.API[c.CREATE], {"name": name, "login": login}).text) + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"creating token '{name}' for user '{login}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) + raise exceptions.ObjectAlreadyExists(name, e.response.text) + return UserToken(endpoint=endpoint, login=data["login"], json_data=data, name=name) + def __str__(self) -> str: """ :return: Token string representation - :rtype: str """ return f"token '{self.name}' of user '{self.login}'" def revoke(self) -> bool: """Revokes the token :return: Whether the revocation succeeded - :rtype: bool """ - if self.name is None: - return False - log.info("Revoking token '%s' of user login '%s'", self.name, self.login) - return self.post(UserToken.API_REVOKE, {"name": self.name, "login": self.login}).ok + return self.delete() + + def api_params(self, op: str = c.GET) -> types.ApiParams: + """Return params used to search/create/delete for that object""" + ops = {c.GET: {"name": self.name, "login": self.login}} + return ops[op] if op in ops else ops[c.GET] def audit(self, settings: types.ConfigSettings, today: Optional[datetime.datetime] = None) -> list[Problem]: + """Audits a token + + :return: List of problem found + """ problems = [] mode = settings.get("audit.mode", "") if not today: @@ -96,18 +119,8 @@ def audit(self, settings: types.ConfigSettings, today: Optional[datetime.datetim def search(endpoint: pf.Platform, login: str) -> list[UserToken]: """Searches tokens of a given user - :param str login: login of the user + :param login: login of the user :return: list of tokens - :rtype: list[UserToken] """ - data = json.loads(endpoint.get(UserToken.API_SEARCH, {"login": login}).text) + data = json.loads(endpoint.get(UserToken.API[c.LIST], {"login": login}).text) return [UserToken(endpoint=endpoint, login=data["login"], json_data=tk) for tk in data["userTokens"]] - - -def generate(name: str, endpoint: pf.Platform, login: str = None) -> UserToken: - """Generates a new token for a given user - :return: the generated Token object - :rtype: Token - """ - data = json.loads(endpoint.post(UserToken.API_GENERATE, {"name": name, "login": login}).text) - return UserToken(endpoint=endpoint, login=data["login"], json_data=data) diff --git a/sonar/users.py b/sonar/users.py index cd245b77d..8a9cb785e 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -60,6 +60,7 @@ class User(sqobject.SqObject): c.CREATE: "users/create", c.UPDATE: "users/update", c.SEARCH: "v2/users-management/users", + "GROUP_MEMBERSHIPS": "v2/authorizations/group-memberships", "DEACTIVATE": "users/deactivate", "UPDATE_LOGIN": "users/update_login", } @@ -185,12 +186,12 @@ def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: if self._groups is not None and kwargs.get(c.USE_CACHE, True): return self._groups if self.endpoint.is_sonarcloud(): - data = json.loads(self.get(_GROUPS_API_SC, {"login": self.key}).text)["groups"] + data = json.loads(self.get(_GROUPS_API_SC, self.api_params(c.GET)).text)["groups"] self._groups = [g["name"] for g in data] elif self.endpoint.version() < (10, 4, 0): self._groups = data.get("groups", []) #: User groups else: - data = json.loads(self.get(_GROUPS_API_V2, {"userId": self._id, "pageSize": 500}).text)["groupMemberships"] + data = json.loads(self.get(User.API["GROUP_MEMBERSHIPS"], {"userId": self._id, "pageSize": 500}).text)["groupMemberships"] log.debug("Groups = %s", str(data)) self._groups = [groups.get_object_from_id(self.endpoint, g["groupId"]).name for g in data] return self._groups @@ -223,7 +224,7 @@ def deactivate(self) -> bool: :return: Whether the deactivation succeeded :rtype: bool """ - return self.post(User.API["DEACTIVATE"], {"name": self.name, "login": self.login}).ok + return self.post(User.API["DEACTIVATE"], self.api_params(User.API["DEACTIVATE"])).ok def tokens(self, **kwargs) -> list[tokens.UserToken]: """ @@ -250,7 +251,7 @@ def update(self, **kwargs) -> User: :rtype: User """ log.debug("Updating %s with %s", str(self), str(kwargs)) - params = {"login": self.login} + params = self.api_params(c.UPDATE) my_data = vars(self) if self.is_local: params.update({k: kwargs[k] for k in ("name", "email") if k in kwargs and kwargs[k] != my_data[k]}) @@ -262,7 +263,7 @@ def update(self, **kwargs) -> User: new_login = kwargs["login"] o = User.CACHE.get(new_login, self.endpoint.url) if not o: - self.post(User.API["UPDATE_LOGIN"], params={"login": self.login, "newLogin": new_login}) + self.post(User.API["UPDATE_LOGIN"], params={**self.api_params(User.API["UPDATE_LOGIN"]), "newLogin": new_login}) User.CACHE.pop(self) self.login = new_login User.CACHE.put(self) @@ -300,6 +301,11 @@ def remove_from_group(self, group_name: str) -> bool: raise exceptions.UnsupportedOperation(f"Group '{group_name}' is built-in, can't remove membership for {str(self)}") return group.remove_user(self.login) + def api_params(self, op: str = c.GET) -> types.ApiParams: + """Return params used to search/create/delete for that object""" + ops = {c.GET: {"login": self.login}} + return ops[op] if op in ops else ops[c.GET] + def set_groups(self, group_list: list[str]) -> bool: """Set the user group membership (replaces current groups) @@ -340,7 +346,7 @@ def set_scm_accounts(self, accounts_list: list[str]) -> bool: :rtype: bool """ log.debug("Setting SCM accounts of %s to '%s'", str(self), str(accounts_list)) - r = self.post(User.API[c.UPDATE], params={"login": self.login, "scmAccount": ",".join(set(accounts_list))}) + r = self.post(User.API[c.UPDATE], params={**self.api_params(c.UPDATE), "scmAccount": ",".join(set(accounts_list))}) if not r.ok: self.scm_accounts = [] return False diff --git a/sonar/utilities.py b/sonar/utilities.py index 8e3f9a67e..7174e5981 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -402,7 +402,7 @@ def nbr_total_elements(sonar_api_json: dict[str, str]) -> int: elif "paging" in sonar_api_json: return sonar_api_json["paging"]["total"] else: - return 1 + return 0 @contextlib.contextmanager From f83cffbfd06cbc2d5cf79075fc094fd0944f9538 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Tue, 24 Dec 2024 11:10:50 +0100 Subject: [PATCH 08/22] Fix pageSize and page parameter depending on PAI version (#1538) * Add severity field, re-added with 10.8 * Produce riight page and pageSize parameter depending on API version * Adjust to new generic issue report format * Fix for trivy scan * Add issue type * Move get_object in class * Add test for more than 50 users and groups * Quality pass * Quality pass * Add API v2 handling * Add is_api_v2() utility * Add api v1 / v2 discrimination * Fix add_user for api v2 * Fix release after which MQR was added * Add delete for API v2 * Fix delete * Remove useless data parameter to get/post * Formatting * Create groups if not exists * Type Hints * Type hints * Dump data as str, pass headers * Formatting * Fix call to groups.add_user() * Fix add_user() * Prettier printing of BODY * Fix remove_from_group() * Cleaner remove_user() * Fixes * Add delete() (actually deactivate()) * Fixes * Fixes * Fixes #1539 * Quality pass * REname _id into id * Quality pass * Quality pass * Lower min nbr of issues since project is improving --- conf/scan.sh | 19 ++--- conf/shellcheck2sonar.py | 11 ++- conf/trivy2sonar.py | 7 +- sonar/groups.py | 164 ++++++++++++++++++++++++++------------- sonar/platform.py | 52 ++++++++----- sonar/rules.py | 2 +- sonar/sqobject.py | 28 +++---- sonar/users.py | 90 ++++++++++++++------- sonar/utilities.py | 19 +++-- sonar/webhooks.py | 2 +- test/conftest.py | 34 +++++++- test/test_findings.py | 5 +- test/test_groups.py | 56 +++++++++++++ test/test_projects.py | 2 +- test/test_users.py | 14 +++- 15 files changed, 358 insertions(+), 147 deletions(-) create mode 100644 test/test_groups.py diff --git a/conf/scan.sh b/conf/scan.sh index e5f95fd4e..99c7ff598 100755 --- a/conf/scan.sh +++ b/conf/scan.sh @@ -23,9 +23,9 @@ ME="$( basename "${BASH_SOURCE[0]}" )" ROOTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" CONFDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -dolint=true -dotest=false -localbuild=false +dolint="true" +dotest="false" +localbuild="false" scanOpts=() @@ -33,11 +33,11 @@ while [ $# -ne 0 ] do case "$1" in -nolint) - dolint=false + dolint="false" ;; -test) - dotest=true - localbuild=true + dotest="true" + localbuild="true" ;; *) scanOpts=("${scanOpts[@]}" "$1") @@ -58,14 +58,15 @@ utReport="$buildDir/xunit-results.xml" [ ! -d $buildDir ] && mkdir $buildDir rm -rf -- ${buildDir:?"."}/* .coverage */__pycache__ */*.pyc # mediatools/__pycache__ testpytest/__pycache__ testunittest/__pycache__ -if [ "$dotest" == "true" ]; then - "$CONFDIR"/run_tests.sh -fi if [ "$dolint" != "false" ]; then "$CONFDIR"/run_linters.sh "$localbuild" fi +if [ "$dotest" == "true" ]; then + "$CONFDIR"/run_tests.sh +fi + version=$(grep PACKAGE_VERSION $ROOTDIR/sonar/version.py | cut -d "=" -f 2 | sed -e "s/[\'\" ]//g" -e "s/^ +//" -e "s/ +$//") cmd="sonar-scanner -Dsonar.projectVersion=$version \ diff --git a/conf/shellcheck2sonar.py b/conf/shellcheck2sonar.py index 0ca16762f..17a5ae955 100755 --- a/conf/shellcheck2sonar.py +++ b/conf/shellcheck2sonar.py @@ -27,6 +27,7 @@ import json SHELLCHECK = "shellcheck" +MAPPING = {"INFO": "INFO", "LOW": "MINOR", "MEDIUM": "MAJOR", "HIGH": "CRITICAL", "BLOCKER": "BLOCKER"} def main() -> None: @@ -53,17 +54,19 @@ def main() -> None: } issue_list.append(sonar_issue) if issue["level"] in ("info", "style"): - sev = "LOW" + sev_mqr = "LOW" elif issue["level"] == "warning": - sev = "MEDIUM" + sev_mqr = "MEDIUM" else: - sev = "HIGH" + sev_mqr = "HIGH" rules_dict[f"{SHELLCHECK}:{issue['code']}"] = { "id": f"{SHELLCHECK}:{issue['code']}", "name": f"{SHELLCHECK}:{issue['code']}", "engineId": SHELLCHECK, + "type": "CODE_SMELL", "cleanCodeAttribute": "LOGICAL", - "impacts": [{"softwareQuality": "MAINTAINABILITY", "severity": sev}], + "severity": MAPPING[sev_mqr], + "impacts": [{"softwareQuality": "MAINTAINABILITY", "severity": sev_mqr}], } external_issues = {"rules": list(rules_dict.values()), "issues": issue_list} diff --git a/conf/trivy2sonar.py b/conf/trivy2sonar.py index a9831d007..9001e454d 100755 --- a/conf/trivy2sonar.py +++ b/conf/trivy2sonar.py @@ -28,6 +28,8 @@ TOOLNAME = "trivy" +MAPPING = {"INFO": "INFO", "LOW": "MINOR", "MEDIUM": "MAJOR", "HIGH": "CRITICAL", "BLOCKER": "BLOCKER"} + def main() -> None: """Main script entry point""" @@ -60,15 +62,16 @@ def main() -> None: # sev = "MEDIUM" # else: # sev = "HIGH" - sev = issue.get("Severity", "MEDIUM") + sev_mqr = issue.get("Severity", "MEDIUM") rules_dict[f"{TOOLNAME}:{issue['VulnerabilityID']}"] = { "id": f"{TOOLNAME}:{issue['VulnerabilityID']}", "name": f"{TOOLNAME}:{issue['VulnerabilityID']} - {issue['Title']}", "description": issue.get("Description", ""), "engineId": TOOLNAME, "type": "VULNERABILITY", + "severity": MAPPING[sev_mqr], "cleanCodeAttribute": "LOGICAL", - "impacts": [{"softwareQuality": "SECURITY", "severity": sev}], + "impacts": [{"softwareQuality": "SECURITY", "severity": sev_mqr}], } external_issues = {"rules": list(rules_dict.values()), "issues": issue_list} diff --git a/sonar/groups.py b/sonar/groups.py index e8a9e3516..e0a3e80eb 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -38,8 +38,10 @@ from sonar.util import types, cache, constants as c SONAR_USERS = "sonar-users" - - +ADD_USER = "ADD_USER" +REMOVE_USER = "REMOVE_USER" +GROUPS_API = "v2/authorizations/groups" +MEMBERSHIP_API = "v2/authorizations/group-memberships" class Group(sq.SqObject): """ Abstraction of the SonarQube "group" concept. @@ -47,14 +49,22 @@ class Group(sq.SqObject): """ CACHE = cache.Cache() - SEARCH_API_V1 = "user_groups/search" - UPDATE_API_V1 = "user_groups/update" + API = { + c.CREATE: GROUPS_API, + c.UPDATE: GROUPS_API, + c.DELETE: GROUPS_API, + c.SEARCH: GROUPS_API, + ADD_USER: MEMBERSHIP_API, + REMOVE_USER: MEMBERSHIP_API, + } + API_V1 = { c.CREATE: "user_groups/create", - c.UPDATE: "v2/authorizations/groups", - c.SEARCH: "v2/authorizations/groups", - "ADD_USER": "user_groups/add_user", - "REMOVE_USER": "user_groups/remove_user", + c.UPDATE: "user_groups/update", + c.DELETE: "user_groups/delete", + c.SEARCH: "user_groups/search", + ADD_USER: "user_groups/add_user", + REMOVE_USER: "user_groups/remove_user", } SEARCH_KEY_FIELD = "name" SEARCH_RETURN_FIELD = "groups" @@ -66,10 +76,10 @@ def __init__(self, endpoint: pf.Platform, name: str, data: types.ApiPayload) -> self.description = data.get("description", "") #: Group description self.__members_count = data.get("membersCount", None) self.__is_default = data.get("default", None) - self._id = data.get("id", None) #: SonarQube 10.4+ Group id + self.id = data.get("id", None) #: SonarQube 10.4+ Group id self.sq_json = data Group.CACHE.put(self) - log.debug("Created %s object", str(self)) + log.debug("Created %s object, id %s", str(self), str(self.id)) @classmethod def read(cls, endpoint: pf.Platform, name: str) -> Group: @@ -83,7 +93,7 @@ def read(cls, endpoint: pf.Platform, name: str) -> Group: o = Group.CACHE.get(name, endpoint.url) if o: return o - data = util.search_by_name(endpoint, name, Group.get_search_api(endpoint), "groups") + data = util.search_by_name(endpoint, name, Group._api_for(c.SEARCH, endpoint), "groups") if data is None: raise exceptions.ObjectNotFound(name, f"Group '{name}' not found.") # SonarQube 10 compatibility: "id" field is dropped, use "name" instead @@ -96,15 +106,17 @@ def read(cls, endpoint: pf.Platform, name: str) -> Group: def create(cls, endpoint: pf.Platform, name: str, description: str = None) -> Group: """Creates a new group in SonarQube and returns the corresponding Group object - :param Platform endpoint: Reference to the SonarQube platform - :param str name: Group name - :param description: Group description - :type description: str, optional + :param endpoint: Reference to the SonarQube platform + :param name: Group name + :param description: Group description, optional :return: The group object - :rtype: Group or None """ log.debug("Creating group '%s'", name) - endpoint.post(Group.API[c.SEARCH], params={"name": name, "description": description}) + try: + endpoint.post(Group._api_for(c.CREATE, endpoint), params={"name": name, "description": description}) + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"creating group '{name}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) + raise exceptions.ObjectAlreadyExists(name, util.sonar_error(e.response)) return cls.read(endpoint=endpoint, name=name) @classmethod @@ -112,18 +124,55 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> Group: """Creates a Group object from the result of a SonarQube API group search data :param Platform endpoint: Reference to the SonarQube platform - :param dict data: The JSON data corresponding to the group + :param data: The JSON data corresponding to the group :return: The group object - :rtype: Group or None """ return cls(endpoint=endpoint, name=data["name"], data=data) @classmethod - def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.API[c.SEARCH] - if endpoint.version() < (10, 4, 0): - api = cls.SEARCH_API_V1 - return api + def _api_for(cls, op: str, endpoint: object) -> Optional[str]: + """Returns the API for a given operation depending on the SonarQube version""" + return cls.API[op] if endpoint.version() >= (10, 4, 0) else cls.API_V1[op] + + @classmethod + def get_object(cls, endpoint: pf.Platform, name: str) -> Group: + """Returns a group object + + :param Platform endpoint: reference to the SonarQube platform + :param str name: group name + :return: The group + """ + o = Group.CACHE.get(name, endpoint.url) + if not o: + get_list(endpoint) + o = Group.CACHE.get(name, endpoint.url) + if not o: + raise exceptions.ObjectNotFound(name, message=f"Group '{name}' not found") + return o + + def delete(self) -> bool: + """Deletes an object, returns whether the operation succeeded""" + log.info("Deleting %s", str(self)) + try: + if self.endpoint.version() >= (10, 4, 0): + ok = self.endpoint.delete(api=f"{Group.API[c.DELETE]}/{self.id}").ok + else: + ok = self.post(api=Group.API_V1[c.DELETE], params=self.api_params(c.DELETE)).ok + if ok: + log.info("Removing from %s cache", str(self.__class__.__name__)) + self.__class__.CACHE.pop(self) + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"deleting {str(self)}", catch_http_errors=(HTTPStatus.NOT_FOUND,)) + raise exceptions.ObjectNotFound(self.key, f"{str(self)} not found") + return ok + + def api_params(self, op: str) -> types.ApiParams: + """Return params used to search/create/delete for that object""" + if self.endpoint.version() >= (10, 4, 0): + ops = {c.GET: {}} + else: + ops = {c.GET: {"name": self.name}} + return ops[op] if op in ops else ops[c.GET] def __str__(self) -> str: """ @@ -154,15 +203,19 @@ def url(self) -> str: """ return f"{self.endpoint.url}/admin/groups" - def add_user(self, user_login: str) -> bool: + def add_user(self, user: object) -> bool: """Adds a user in the group - :param str user_login: User login + :param user: the User to add :return: Whether the operation succeeded - :rtype: bool """ + log.info("Adding %s to %s", str(user), str(self)) try: - r = self.post(Group.API["ADD_USER"], params={"login": user_login, "name": self.name}) + if self.endpoint.version() >= (10, 4, 0): + params = {"groupId": self.id, "userId": user.id} + else: + params = {"login": user.login, "name": self.name} + r = self.post(Group._api_for(ADD_USER, self.endpoint), params=params) except (ConnectionError, RequestException) as e: util.handle_error(e, "adding user to group") if isinstance(e, HTTPError): @@ -170,17 +223,34 @@ def add_user(self, user_login: str) -> bool: if code == HTTPStatus.BAD_REQUEST: raise exceptions.UnsupportedOperation(util.sonar_error(e.response)) if code == HTTPStatus.NOT_FOUND: - raise exceptions.ObjectNotFound(user_login, util.sonar_error(e.response)) + raise exceptions.ObjectNotFound(user.login, util.sonar_error(e.response)) return r.ok - def remove_user(self, user_login: str) -> bool: + def remove_user(self, user: object) -> bool: """Removes a user from the group :param str user_login: User login :return: Whether the operation succeeded :rtype: bool """ - return self.post(Group.API["REMOVE_USER"], params={"login": user_login, "name": self.name}).ok + log.info("Removing %s from %s", str(user), str(self)) + try: + if self.endpoint.version() >= (10, 4, 0): + for m in json.loads(self.get(MEMBERSHIP_API, params={"userId": user.id}).text)["groupMemberships"]: + if m["groupId"] == self.id: + return self.endpoint.delete(f"{Group._api_for(REMOVE_USER, self.endpoint)}/{m['id']}").ok + else: + params = {"login": user.login, "name": self.name} + return self.post(Group._api_for(REMOVE_USER, self.endpoint), params=params).ok + except (ConnectionError, RequestException) as e: + util.handle_error(e, "removing user from group") + if isinstance(e, HTTPError): + code = e.response.status_code + if code == HTTPStatus.BAD_REQUEST: + raise exceptions.UnsupportedOperation(util.sonar_error(e.response)) + if code == HTTPStatus.NOT_FOUND: + raise exceptions.ObjectNotFound(user.login, util.sonar_error(e.response)) + return False def audit(self, audit_settings: types.ConfigSettings = None) -> list[Problem]: """Audits a group and return list of problems found @@ -227,9 +297,9 @@ def set_description(self, description: str) -> bool: log.debug("Updating %s with description = %s", str(self), description) if self.endpoint.version() >= (10, 4, 0): data = json.dumps({"description": description}) - r = self.patch(f"{Group.API[c.UPDATE]}/{self._id}", data=data, headers={"content-type": "application/merge-patch+json"}) + r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", data=data) else: - r = self.post(Group.UPDATE_API_V1, params={"currentName": self.key, "description": description}) + r = self.post(Group.API_V1[c.UPDATE], params={"currentName": self.key, "description": description}) if r.ok: self.description = description return r.ok @@ -246,9 +316,9 @@ def set_name(self, name: str) -> bool: return True log.debug("Updating %s with name = %s", str(self), name) if self.endpoint.version() >= (10, 4, 0): - r = self.patch(f"{Group.API[c.UPDATE]}/{self.key}", params={"name": name}) + r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", params={"name": name}) else: - r = self.post(Group.UPDATE_API_V1, params={"currentName": self.key, "name": name}) + r = self.post(Group.API[c.UPDATE], params={"currentName": self.key, "name": name}) if r.ok: Group.CACHE.pop(self) self.name = name @@ -264,7 +334,7 @@ def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, G :return: dict of groups with group name as key :rtype: dict{name: Group} """ - return sq.search_objects(endpoint=endpoint, object_class=Group, params=params) + return sq.search_objects(endpoint=endpoint, object_class=Group, params=params, api_version=2) def get_list(endpoint: pf.Platform) -> dict[str, Group]: @@ -319,22 +389,6 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) return problems -def get_object(endpoint: pf.Platform, name: str) -> Group: - """Returns a group object - - :param Platform endpoint: reference to the SonarQube platform - :param str name: group name - :return: The group - """ - o = Group.CACHE.get(name, endpoint.url) - if not o: - get_list(endpoint) - o = Group.CACHE.get(name, endpoint.url) - if not o: - raise exceptions.ObjectNotFound(name, message=f"Group '{name}' not found") - return o - - def get_object_from_id(endpoint: pf.Platform, id: str) -> Group: """Searches a Group object from its id - SonarQube 10.4+""" if endpoint.version() < (10, 4, 0): @@ -342,7 +396,7 @@ def get_object_from_id(endpoint: pf.Platform, id: str) -> Group: if len(Group.CACHE) == 0: get_list(endpoint) for o in Group.CACHE.values(): - if o._id == id: + if o.id == id: return o raise exceptions.ObjectNotFound(id, message=f"Group '{id}' not found") @@ -357,7 +411,7 @@ def create_or_update(endpoint: pf.Platform, name: str, description: str) -> Grou :rtype: Group """ try: - o = get_object(endpoint=endpoint, name=name) + o = Group.get_object(endpoint=endpoint, name=name) o.set_description(description) return o except exceptions.ObjectNotFound: @@ -392,7 +446,7 @@ def exists(group_name: str, endpoint: pf.Platform) -> bool: :return: whether the project exists :rtype: bool """ - return get_object(name=group_name, endpoint=endpoint) is not None + return Group.get_object(name=group_name, endpoint=endpoint) is not None def convert_for_yaml(original_json: types.ObjectJsonRepr) -> types.ObjectJsonRepr: diff --git a/sonar/platform.py b/sonar/platform.py index b4aceeee4..3078549bf 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -190,7 +190,14 @@ def post(self, api: str, params: types.ApiParams = None, **kwargs) -> requests.R :param params: params to pass in the HTTP request, defaults to None :return: the HTTP response """ - return self.__run_request(requests.post, api, params, **kwargs) + if util.is_api_v2(api): + if "headers" in kwargs: + kwargs["headers"]["content-type"] = "application/json" + else: + kwargs["headers"] = {"content-type": "application/json"} + return self.__run_request(requests.post, api, data=json.dumps(params), **kwargs) + else: + return self.__run_request(requests.post, api, params, **kwargs) def patch(self, api: str, params: types.ApiParams = None, **kwargs) -> requests.Response: """Makes an HTTP PATCH request to SonarQube @@ -199,7 +206,14 @@ def patch(self, api: str, params: types.ApiParams = None, **kwargs) -> requests. :param params: params to pass in the HTTP request, defaults to None :return: the HTTP response """ - return self.__run_request(requests.patch, api, params, **kwargs) + if util.is_api_v2(api): + if "headers" in kwargs: + kwargs["headers"]["content-type"] = "application/merge-patch+json" + else: + kwargs["headers"] = {"content-type": "application/merge-patch+json"} + return self.__run_request(requests.patch, api=api, data=json.dumps(params), **kwargs) + else: + return self.__run_request(requests.patch, api, params, **kwargs) def delete(self, api: str, params: types.ApiParams = None, **kwargs) -> requests.Response: """Makes an HTTP DELETE request to SonarQube @@ -214,7 +228,8 @@ def __run_request(self, request: callable, api: str, params: types.ApiParams = N """Makes an HTTP request to SonarQube""" mute = kwargs.pop("mute", ()) api = _normalize_api(api) - headers = {"user-agent": self._user_agent, **kwargs.get("headers", {})} + headers = {"user-agent": self._user_agent} + headers.update(kwargs.get("headers", {})) if params is None: params = {} with_org = kwargs.pop("with_organization", True) @@ -225,9 +240,9 @@ def __run_request(self, request: callable, api: str, params: types.ApiParams = N req_type, url = "", "" if log.get_level() <= log.DEBUG: req_type = getattr(request, "__name__", repr(request)).upper() - url = self.__urlstring(api, params) + url = self.__urlstring(api, params, kwargs.get("data", {})) log.debug("%s: %s", req_type, url) - + kwargs["headers"] = headers try: retry = True while retry: @@ -237,7 +252,6 @@ def __run_request(self, request: callable, api: str, params: types.ApiParams = N auth=self.__credentials(), verify=self.__cert_file, params=params, - headers=headers, timeout=self.http_timeout, **kwargs, ) @@ -374,20 +388,22 @@ def set_setting(self, key: str, value: any) -> bool: """ return settings.set_setting(self, key, value) - def __urlstring(self, api: str, params: types.ApiParams) -> str: + def __urlstring(self, api: str, params: types.ApiParams, data: str = None) -> str: """Returns a string corresponding to the URL and parameters""" url = f"{str(self)}{api}" - if params is None: - return url - good_params = {k: v for k, v in params.items() if v is not None} - if len(good_params) == 0: - return url - for k, v in good_params.items(): - if isinstance(v, datetime.date): - good_params[k] = util.format_date(v) - elif isinstance(v, (list, tuple, set)): - good_params[k] = ",".join(list(v)) - return url + "?" + "&".join([f"{k}={requests.utils.quote(str(v))}" for k, v in good_params.items()]) + if params is not None: + good_params = {k: v for k, v in params.items() if v is not None} + for k, v in good_params.items(): + if isinstance(v, datetime.date): + good_params[k] = util.format_date(v) + elif isinstance(v, (list, tuple, set)): + good_params[k] = ",".join(list(v)) + params_string = "&".join([f"{k}={requests.utils.quote(str(v))}" for k, v in good_params.items()]) + if len(params_string) > 0: + url += f"?{params_string}" + if data is not None: + url += f" - BODY: {data}" + return url def webhooks(self) -> dict[str, object]: """ diff --git a/sonar/rules.py b/sonar/rules.py index 49bc054cc..128a82c29 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -263,7 +263,7 @@ def to_csv(self) -> list[str]: data["ruleType"] = "TEMPLATE" elif self.template_key: data["ruleType"] = "INSTANTIATED" - if self.endpoint.version() > (10, 4, 0): + if self.endpoint.version() >= (10, 4, 0): data["legacySeverity"] = data.pop("severity", "") data["legacyType"] = data.pop("type", "") for qual in QUALITIES: diff --git a/sonar/sqobject.py b/sonar/sqobject.py index 99e8f2716..cb5a3c726 100644 --- a/sonar/sqobject.py +++ b/sonar/sqobject.py @@ -112,7 +112,6 @@ def post( self, api: str, params: types.ApiParams = None, - data: str = None, mute: tuple[HTTPStatus] = (), **kwargs, ) -> requests.Response: @@ -125,13 +124,12 @@ def post( :type mute: tuple, optional :return: The request response """ - return self.endpoint.post(api=api, params=params, data=data, mute=mute, **kwargs) + return self.endpoint.post(api=api, params=params, mute=mute, **kwargs) def patch( self, api: str, params: types.ApiParams = None, - data: str = None, mute: tuple[HTTPStatus] = (), **kwargs, ) -> requests.Response: @@ -144,7 +142,7 @@ def patch( :type mute: tuple, optional :return: The request response """ - return self.endpoint.patch(api=api, params=params, data=data, mute=mute, **kwargs) + return self.endpoint.patch(api=api, params=params, mute=mute, **kwargs) def delete(self) -> bool: """Deletes an object, returns whether the operation succeeded""" @@ -198,14 +196,13 @@ def get_tags(self, **kwargs) -> list[str]: def __search_thread(queue: Queue) -> None: """Performs a search for a given object""" while not queue.empty(): - (endpoint, api, objects, key_field, returned_field, object_class, params, page) = queue.get() + (endpoint, api, objects, key_field, returned_field, object_class, params) = queue.get() page_params = params.copy() - page_params["p"] = page log.debug("Threaded search: API = %s params = %s", api, str(params)) try: data = json.loads(endpoint.get(api, params=page_params).text) for obj in data[returned_field]: - if object_class.__name__ in ("QualityProfile", "QualityGate", "Groups", "Portfolio", "Project"): + if object_class.__name__ in ("Portfolio", "Group", "QualityProfile", "User", "Application", "Project", "Organization"): objects[obj[key_field]] = object_class.load(endpoint=endpoint, data=obj) else: objects[obj[key_field]] = object_class(endpoint, obj[key_field], data=obj) @@ -214,20 +211,22 @@ def __search_thread(queue: Queue) -> None: queue.task_done() -def search_objects(endpoint: object, object_class: any, params: types.ApiParams, threads: int = 8) -> dict[str, SqObject]: +def search_objects(endpoint: object, object_class: any, params: types.ApiParams, threads: int = 8, api_version: int = 1) -> dict[str, SqObject]: """Runs a multi-threaded object search for searchable Sonar Objects""" api = object_class.get_search_api(endpoint) key_field = object_class.SEARCH_KEY_FIELD returned_field = object_class.SEARCH_RETURN_FIELD new_params = {} if params is None else params.copy() - if "ps" not in new_params: - new_params["ps"] = 500 - new_params["p"] = 1 + p_field = "pageIndex" if api_version == 2 else "p" + ps_field = "pageSize" if api_version == 2 else "ps" + if ps_field not in new_params: + new_params[ps_field] = 500 + new_params[p_field] = 1 objects_list = {} data = json.loads(endpoint.get(api, params=new_params).text) - nb_pages = utilities.nbr_pages(data) - nb_objects = max(len(data[returned_field]), utilities.nbr_total_elements(data)) + nb_pages = utilities.nbr_pages(data, api_version) + nb_objects = max(len(data[returned_field]), utilities.nbr_total_elements(data, api_version)) log.debug("Loading %d %ss page of %d elements...", nb_objects, object_class.__name__, len(data[returned_field])) if utilities.nbr_total_elements(data) > 0 and len(data[returned_field]) == 0: msg = f"Index on {object_class.__name__} is corrupted, please reindex before using API" @@ -243,7 +242,8 @@ def search_objects(endpoint: object, object_class: any, params: types.ApiParams, return objects_list q = Queue(maxsize=0) for page in range(2, nb_pages + 1): - q.put((endpoint, api, objects_list, key_field, returned_field, object_class, new_params, page)) + new_params[p_field] = page + q.put((endpoint, api, objects_list, key_field, returned_field, object_class, new_params)) for i in range(threads): log.debug("Starting %s search thread %d", object_class.__name__, i) worker = Thread(target=__search_thread, args=[q]) diff --git a/sonar/users.py b/sonar/users.py index 8a9cb785e..b30acb8f2 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -39,7 +39,6 @@ _GROUPS_API_SC = "users/groups" -_GROUPS_API_V2 = "v2/authorizations/group-memberships" SETTABLE_PROPERTIES = ("login", "name", "scmAccounts", "email", "groups", "local") @@ -51,7 +50,6 @@ class User(sqobject.SqObject): """ CACHE = cache.Cache() - SEARCH_API_V1 = "users/search" SEARCH_KEY_FIELD = "login" SEARCH_RETURN_FIELD = "users" @@ -59,9 +57,16 @@ class User(sqobject.SqObject): API = { c.CREATE: "users/create", c.UPDATE: "users/update", + c.DELETE: "v2/users-management/users", c.SEARCH: "v2/users-management/users", "GROUP_MEMBERSHIPS": "v2/authorizations/group-memberships", - "DEACTIVATE": "users/deactivate", + "UPDATE_LOGIN": "users/update_login", + } + API_V1 = { + c.CREATE: "users/create", + c.UPDATE: "users/update", + c.DELETE: "users/deactivate", + c.SEARCH: "users/search", "UPDATE_LOGIN": "users/update_login", } @@ -69,7 +74,7 @@ def __init__(self, endpoint: pf.Platform, login: str, data: types.ApiPayload) -> """Do not use to create users, use on of the constructor class methods""" super().__init__(endpoint=endpoint, key=login) self.login = login #: User login (str) - self._id = None #: SonarQube 10+ User Id (str) + self.id = None #: SonarQube 10+ User Id (str) self.name = None #: User name (str) self._groups = None #: User groups (list) self.scm_accounts = None #: User SCM accounts (list) @@ -79,7 +84,7 @@ def __init__(self, endpoint: pf.Platform, login: str, data: types.ApiPayload) -> self.nb_tokens = None #: Nbr of tokens (int) - read-only self.__tokens = None self.__load(data) - log.debug("Created %s", str(self)) + log.debug("Created %s id '%s'", str(self), str(self.id)) User.CACHE.put(self) @classmethod @@ -97,7 +102,7 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> User: return cls(login=data["login"], endpoint=endpoint, data=data) @classmethod - def create(cls, endpoint: pf.Platform, login: str, name: str = None, is_local: bool = True, password: str = None) -> User: + def create(cls, endpoint: pf.Platform, login: str, name: str, is_local: bool = True, password: str = None) -> User: """Creates a new user in SonarQube and returns the corresponding User object :param Platform endpoint: Reference to the SonarQube platform @@ -111,7 +116,7 @@ def create(cls, endpoint: pf.Platform, login: str, name: str = None, is_local: b :return: The user object :rtype: User or None """ - log.debug("Creating user '%s'", login) + log.debug("Creating user '%s' name '%s'", login, name) params = {"login": login, "local": str(is_local).lower(), "name": name} if is_local: params["password"] = password if password else login @@ -142,13 +147,9 @@ def get_object(cls, endpoint: pf.Platform, login: str) -> User: raise exceptions.ObjectNotFound(login, f"User '{login}' not found") @classmethod - def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.SEARCH_API_V1 - if endpoint.is_sonarcloud(): - api = cls.SEARCH_API_SC - elif endpoint.version() >= (10, 4, 0): - api = cls.API[c.SEARCH] - return api + def _api_for(cls, op: str, endpoint: object) -> Optional[str]: + """Returns the API for a given operation depedning on the SonarQube version""" + return cls.API[op] if endpoint.version() >= (10, 4, 0) else cls.API_V1[op] def __str__(self) -> str: """ @@ -176,13 +177,14 @@ def __load(self, data: types.ApiPayload) -> None: self.last_login = dt1 else: self.last_login = max(dt1, dt2) - self._id = data["id"] + self.id = data["id"] self.__tokens = None self._groups = self.groups(data) #: User groups self.sq_json = data def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: """Returns the list of groups of a user""" + log.info("Getting %s groups = %s", str(self), str(self._groups)) if self._groups is not None and kwargs.get(c.USE_CACHE, True): return self._groups if self.endpoint.is_sonarcloud(): @@ -191,7 +193,7 @@ def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: elif self.endpoint.version() < (10, 4, 0): self._groups = data.get("groups", []) #: User groups else: - data = json.loads(self.get(User.API["GROUP_MEMBERSHIPS"], {"userId": self._id, "pageSize": 500}).text)["groupMemberships"] + data = json.loads(self.get(User.API["GROUP_MEMBERSHIPS"], {"userId": self.id, "pageSize": 500}).text)["groupMemberships"] log.debug("Groups = %s", str(data)) self._groups = [groups.get_object_from_id(self.endpoint, g["groupId"]).name for g in data] return self._groups @@ -201,8 +203,7 @@ def refresh(self) -> User: :return: The user itself """ - api = User.get_search_api(self.endpoint) - data = json.loads(self.get(api, params={"q": self.login}).text) + data = json.loads(self.get(User._api_for(c.SEARCH, self.endpoint), params={"q": self.login}).text) for d in data["users"]: if d["login"] == self.login: self.__load(d) @@ -218,14 +219,6 @@ def url(self) -> str: """ return f"{self.endpoint.url}/admin/users" - def deactivate(self) -> bool: - """Deactivates the user - - :return: Whether the deactivation succeeded - :rtype: bool - """ - return self.post(User.API["DEACTIVATE"], self.api_params(User.API["DEACTIVATE"])).ok - def tokens(self, **kwargs) -> list[tokens.UserToken]: """ :return: The list of tokens of the user @@ -282,7 +275,7 @@ def add_to_group(self, group_name: str) -> bool: except exceptions.ObjectNotFound: log.warning("Group '%s' does not exists, can't add membership for %s", group_name, str(self)) raise - ok = group.add_user(self.login) + ok = group.add_user(self) if ok: self._groups.append(group_name) return ok @@ -299,7 +292,36 @@ def remove_from_group(self, group_name: str) -> bool: group = groups.Group.read(endpoint=self.endpoint, name=group_name) if group.is_default(): raise exceptions.UnsupportedOperation(f"Group '{group_name}' is built-in, can't remove membership for {str(self)}") - return group.remove_user(self.login) + ok = group.remove_user(self) + if ok: + self._groups.remove(group_name) + return ok + + def deactivate(self) -> bool: + """Deactivates the user + + :return: Whether the deactivation succeeded + """ + return self.delete() + + def delete(self) -> bool: + """Deactivates the user (true deleting is not possible) + + :return: Whether the deactivation succeeded + """ + log.info("Deleting %s", str(self)) + try: + if self.endpoint.version() >= (10, 4, 0): + ok = self.endpoint.delete(api=f"{User.API[c.DELETE]}/{self.id}").ok + else: + ok = self.post(api=User.API_V1[c.DELETE], params=self.api_params(c.DELETE)).ok + if ok: + log.info("Removing from %s cache", str(self.__class__.__name__)) + self.__class__.CACHE.pop(self) + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"deleting {str(self)}", catch_http_errors=(HTTPStatus.NOT_FOUND,)) + raise exceptions.ObjectNotFound(self.key, f"{str(self)} not found") + return ok def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" @@ -409,7 +431,17 @@ def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, U :rtype: dict{login: User} """ log.debug("Searching users with params %s", str(params)) - return dict(sorted(sqobject.search_objects(endpoint=endpoint, object_class=User, params=params).items())) + return dict(sorted(sqobject.search_objects(endpoint=endpoint, object_class=User, params=params, api_version=2).items())) + + +def get_list(endpoint: pf.Platform) -> dict[str, User]: + """Returns the list of users + + :params Platform endpoint: Reference to the SonarQube platform + :return: The list of users + """ + log.info("Listing users") + return search(endpoint) def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> types.ObjectJsonRepr: diff --git a/sonar/utilities.py b/sonar/utilities.py index 7174e5981..c1131c7a5 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -385,26 +385,33 @@ def update_json(json_data: dict[str, str], categ: str, subcateg: str, value: any return json_data -def nbr_pages(sonar_api_json: dict[str, str]) -> int: +def nbr_pages(sonar_api_json: dict[str, str], api_version: int = 1) -> int: """Returns nbr of pages of a paginated Sonar API call""" - if "paging" in sonar_api_json: - return math.ceil(sonar_api_json["paging"]["total"] / sonar_api_json["paging"]["pageSize"]) + paging = "page" if api_version == 2 else "paging" + if paging in sonar_api_json: + return math.ceil(sonar_api_json[paging]["total"] / sonar_api_json[paging]["pageSize"]) elif "total" in sonar_api_json: return math.ceil(sonar_api_json["total"] / sonar_api_json["ps"]) else: return 1 -def nbr_total_elements(sonar_api_json: dict[str, str]) -> int: +def nbr_total_elements(sonar_api_json: dict[str, str], api_version: int = 1) -> int: """Returns nbr of elements of a paginated Sonar API call""" + paging = "page" if api_version == 2 else "paging" if "total" in sonar_api_json: return sonar_api_json["total"] - elif "paging" in sonar_api_json: - return sonar_api_json["paging"]["total"] + elif paging in sonar_api_json: + return sonar_api_json[paging]["total"] else: return 0 +def is_api_v2(api: str) -> bool: + """Returns whether and API string is v2""" + return api.lower().startswith("v2/") or api.lower().startswith("api/v2/") + + @contextlib.contextmanager def open_file(file: str = None, mode: str = "w") -> TextIO: """Opens a file if not None or -, otherwise stdout""" diff --git a/sonar/webhooks.py b/sonar/webhooks.py index 645eb10c4..a6c811b29 100644 --- a/sonar/webhooks.py +++ b/sonar/webhooks.py @@ -87,7 +87,7 @@ def audit(self) -> list[problem.Problem]: """ :meta private: """ - if self.sq_json["latestDelivery"]["success"]: + if "latestDelivery" not in self.sq_json or self.sq_json["latestDelivery"]["success"]: return [] return [problem.Problem(rules.get_rule(rules.RuleId.FAILED_WEBHOOK), self, str(self))] diff --git a/test/conftest.py b/test/conftest.py index 705f712b2..70b07a149 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -26,7 +26,7 @@ import pytest import utilities as util -from sonar import projects, applications, portfolios, qualityprofiles, exceptions, logging, issues, users +from sonar import projects, applications, portfolios, qualityprofiles, exceptions, logging, issues, users, groups TEMP_FILE_ROOT = f"temp.{os.getpid()}" CSV_FILE = f"{TEMP_FILE_ROOT}.csv" @@ -213,3 +213,35 @@ def get_test_application() -> Generator[applications.Application]: o.delete() except exceptions.ObjectNotFound: pass + + +@pytest.fixture +def get_60_groups() -> Generator[list[groups.Group]]: + util.start_logging() + group_list = [] + for i in range(60): + gr_name = f"Group-{util.TEMP_KEY}{i}" + try: + o_gr = groups.Group.get_object(endpoint=util.SQ, name=gr_name) + except exceptions.ObjectNotFound: + o_gr = groups.Group.create(endpoint=util.SQ, name=gr_name, description=gr_name) + group_list.append(o_gr) + yield group_list + for g in group_list: + g.delete() + + +@pytest.fixture +def get_60_users() -> Generator[list[users.User]]: + util.start_logging() + user_list = [] + for i in range(60): + u_name = f"User-{util.TEMP_KEY}{i}" + try: + o_user = users.User.get_object(endpoint=util.SQ, login=u_name) + except exceptions.ObjectNotFound: + o_user = users.User.create(endpoint=util.SQ, login=u_name, name=u_name) + user_list.append(o_user) + yield user_list + for u in user_list: + u.delete() diff --git a/test/test_findings.py b/test/test_findings.py index 8207ce91b..036febbb4 100644 --- a/test/test_findings.py +++ b/test/test_findings.py @@ -370,10 +370,7 @@ def test_issues_count_3() -> None: def test_search_issues_by_project() -> None: """test_search_issues_by_project""" nb_issues = len(issues.search_by_project(endpoint=util.SQ, project_key=util.LIVE_PROJECT, search_findings=True)) - if util.SQ.version() < (10, 0, 0): - assert 200 <= nb_issues <= 1000 - else: - assert 500 <= nb_issues <= 1500 + assert 200 <= nb_issues <= 1000 nb_issues = len(issues.search_by_project(endpoint=util.SQ, project_key=util.LIVE_PROJECT, params={"resolved": "false"})) assert nb_issues < 1000 nb_issues = len(issues.search_by_project(endpoint=util.SQ, project_key=None)) diff --git a/test/test_groups.py b/test/test_groups.py new file mode 100644 index 000000000..a2537a092 --- /dev/null +++ b/test/test_groups.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2024 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +""" Groups tests """ + +from collections.abc import Generator + +import pytest + +import utilities as util +from sonar import exceptions +from sonar import groups + +GROUP1 = "sonar-users" +GROUP2 = "sonar-administrators" + + +def test_get_object() -> None: + """Test group get_obejct""" + for name in GROUP1, GROUP2: + gr = groups.Group.get_object(endpoint=util.SQ, name=name) + assert gr.name == name + assert str(gr) == f"group '{name}'" + + gr2 = groups.Group.get_object(endpoint=util.SQ, name=GROUP2) + assert gr is gr2 + + with pytest.raises(exceptions.ObjectNotFound): + groups.Group.get_object(endpoint=util.SQ, name=util.NON_EXISTING_KEY) + + +def test_more_than_50_groups(get_60_groups: Generator[list[groups.Group]]) -> None: + # Count groups first + group_list = get_60_groups + groups.Group.clear_cache() + new_group_list = groups.get_list(util.SQ) + assert len(new_group_list) > 60 + assert set(new_group_list.keys()) > set(g.name for g in group_list) diff --git a/test/test_projects.py b/test/test_projects.py index 2eb1c25c5..495620ead 100644 --- a/test/test_projects.py +++ b/test/test_projects.py @@ -194,7 +194,7 @@ def test_set_quality_gate(get_test_project: callable) -> None: assert not proj.set_quality_gate(util.EXISTING_QG) -def test_ai_code_assurance(get_test_project: callable) -> None: +def test_ai_code_assurance(get_test_project: Generator[projects.Project]) -> None: """test_set_ai_code_assurance""" proj = get_test_project assert proj.set_contains_ai_code(True) diff --git a/test/test_users.py b/test/test_users.py index 3b508ffcf..a21addef0 100644 --- a/test/test_users.py +++ b/test/test_users.py @@ -19,7 +19,7 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -""" applications tests """ +""" users tests """ from collections.abc import Generator @@ -95,7 +95,8 @@ def test_set_groups(get_test_user: Generator[users.User]) -> None: # TODO(@okorach): Pick groups that exist in SonarQube groups = ["quality-managers", "tech-leads"] for g in groups: - assert user.remove_from_group(g) + if g in user.groups(): + assert user.remove_from_group(g) user.refresh() for g in groups: assert g not in user.groups() @@ -159,3 +160,12 @@ def test_convert_for_yaml() -> None: assert isinstance(json_exp, dict) assert isinstance(yaml_exp, list) assert len(yaml_exp) == len(json_exp) + + +def test_more_than_50_users(get_60_users: Generator[list[users.User]]) -> None: + # Count groups first + user_list = get_60_users + users.User.clear_cache() + new_user_list = users.get_list(util.SQ) + assert len(new_user_list) > 60 + assert set(new_user_list.keys()) > set(u.name for u in user_list) From 0c8153214751ace9fa425bce48ddc6588d81bd26 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Tue, 24 Dec 2024 12:01:37 +0100 Subject: [PATCH 09/22] User-and-group-management-for-sc (#1540) * Add API_SC * Rename _api_for() into api_for() * Rewrite api_for() including SC * Remove unused symbol * Fix * Better docstring * Formatting * Update for scm_accounts() --- sonar/groups.py | 22 ++++++++++++++-------- sonar/sqobject.py | 25 +++++++++++++------------ sonar/users.py | 37 +++++++++++++++++++++++++++---------- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/sonar/groups.py b/sonar/groups.py index e0a3e80eb..8aa360c58 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -42,6 +42,8 @@ REMOVE_USER = "REMOVE_USER" GROUPS_API = "v2/authorizations/groups" MEMBERSHIP_API = "v2/authorizations/group-memberships" + + class Group(sq.SqObject): """ Abstraction of the SonarQube "group" concept. @@ -93,7 +95,7 @@ def read(cls, endpoint: pf.Platform, name: str) -> Group: o = Group.CACHE.get(name, endpoint.url) if o: return o - data = util.search_by_name(endpoint, name, Group._api_for(c.SEARCH, endpoint), "groups") + data = util.search_by_name(endpoint, name, Group.api_for(c.SEARCH, endpoint), "groups") if data is None: raise exceptions.ObjectNotFound(name, f"Group '{name}' not found.") # SonarQube 10 compatibility: "id" field is dropped, use "name" instead @@ -113,7 +115,7 @@ def create(cls, endpoint: pf.Platform, name: str, description: str = None) -> Gr """ log.debug("Creating group '%s'", name) try: - endpoint.post(Group._api_for(c.CREATE, endpoint), params={"name": name, "description": description}) + endpoint.post(Group.api_for(c.CREATE, endpoint), params={"name": name, "description": description}) except (ConnectionError, RequestException) as e: util.handle_error(e, f"creating group '{name}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) raise exceptions.ObjectAlreadyExists(name, util.sonar_error(e.response)) @@ -130,9 +132,13 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> Group: return cls(endpoint=endpoint, name=data["name"], data=data) @classmethod - def _api_for(cls, op: str, endpoint: object) -> Optional[str]: - """Returns the API for a given operation depending on the SonarQube version""" - return cls.API[op] if endpoint.version() >= (10, 4, 0) else cls.API_V1[op] + def api_for(cls, op: str, endpoint: object) -> Optional[str]: + """Returns the API for a given operation depedning on the SonarQube version""" + if endpoint.is_sonarcloud() or endpoint.version() < (10, 4, 0): + api_to_use = Group.API_V1 + else: + api_to_use = Group.API + return api_to_use[op] if op in api_to_use else api_to_use[c.LIST] @classmethod def get_object(cls, endpoint: pf.Platform, name: str) -> Group: @@ -215,7 +221,7 @@ def add_user(self, user: object) -> bool: params = {"groupId": self.id, "userId": user.id} else: params = {"login": user.login, "name": self.name} - r = self.post(Group._api_for(ADD_USER, self.endpoint), params=params) + r = self.post(Group.api_for(ADD_USER, self.endpoint), params=params) except (ConnectionError, RequestException) as e: util.handle_error(e, "adding user to group") if isinstance(e, HTTPError): @@ -238,10 +244,10 @@ def remove_user(self, user: object) -> bool: if self.endpoint.version() >= (10, 4, 0): for m in json.loads(self.get(MEMBERSHIP_API, params={"userId": user.id}).text)["groupMemberships"]: if m["groupId"] == self.id: - return self.endpoint.delete(f"{Group._api_for(REMOVE_USER, self.endpoint)}/{m['id']}").ok + return self.endpoint.delete(f"{Group.api_for(REMOVE_USER, self.endpoint)}/{m['id']}").ok else: params = {"login": user.login, "name": self.name} - return self.post(Group._api_for(REMOVE_USER, self.endpoint), params=params).ok + return self.post(Group.api_for(REMOVE_USER, self.endpoint), params=params).ok except (ConnectionError, RequestException) as e: util.handle_error(e, "removing user from group") if isinstance(e, HTTPError): diff --git a/sonar/sqobject.py b/sonar/sqobject.py index cb5a3c726..c21a557a5 100644 --- a/sonar/sqobject.py +++ b/sonar/sqobject.py @@ -59,20 +59,21 @@ def __eq__(self, another: object) -> bool: return NotImplemented @classmethod - def get_search_api(cls, endpoint: object) -> Optional[str]: - api = cls.API[c.SEARCH] - if endpoint.is_sonarcloud(): - try: - api = cls.SEARCH_API_SC - except AttributeError: - api = cls.API[c.SEARCH] - return api + def api_for(cls, op: str, endpoint: object) -> Optional[str]: + """Returns the API to use for a particular operation + + :param op: The desired API operation + :param endpoint: The SQS or SQC to invoke the API + This function must be overloaded for classes that need specific treatment + e.g. API V1 or V2 depending on SonarQube version, different API for SonarCloud + """ + return cls.API[op] if op in cls.API else cls.API[c.LIST] @classmethod def clear_cache(cls, endpoint: Optional[object] = None) -> None: - """ - Clear the cache of a given class - :param endpoint Platform: Optional, clears only the cache fo rthis platfiorm if specified, clear all if not + """Clears the cache of a given class + + :param endpoint: Optional, clears only the cache fo rthis platfiorm if specified, clear all if not """ log.info("Emptying cache of %s", str(cls)) try: @@ -213,7 +214,7 @@ def __search_thread(queue: Queue) -> None: def search_objects(endpoint: object, object_class: any, params: types.ApiParams, threads: int = 8, api_version: int = 1) -> dict[str, SqObject]: """Runs a multi-threaded object search for searchable Sonar Objects""" - api = object_class.get_search_api(endpoint) + api = object_class.api_for(c.SEARCH, endpoint) key_field = object_class.SEARCH_KEY_FIELD returned_field = object_class.SEARCH_RETURN_FIELD diff --git a/sonar/users.py b/sonar/users.py index b30acb8f2..0266c58be 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -41,6 +41,7 @@ _GROUPS_API_SC = "users/groups" SETTABLE_PROPERTIES = ("login", "name", "scmAccounts", "email", "groups", "local") +USER_API = "v2/users-management/users" class User(sqobject.SqObject): @@ -53,12 +54,11 @@ class User(sqobject.SqObject): SEARCH_KEY_FIELD = "login" SEARCH_RETURN_FIELD = "users" - SEARCH_API_SC = "organizations/search_members" API = { - c.CREATE: "users/create", - c.UPDATE: "users/update", - c.DELETE: "v2/users-management/users", - c.SEARCH: "v2/users-management/users", + c.CREATE: USER_API, + c.UPDATE: USER_API, + c.DELETE: USER_API, + c.SEARCH: USER_API, "GROUP_MEMBERSHIPS": "v2/authorizations/group-memberships", "UPDATE_LOGIN": "users/update_login", } @@ -69,6 +69,9 @@ class User(sqobject.SqObject): c.SEARCH: "users/search", "UPDATE_LOGIN": "users/update_login", } + API_SC = { + c.SEARCH: "organizations/search_members", + } def __init__(self, endpoint: pf.Platform, login: str, data: types.ApiPayload) -> None: """Do not use to create users, use on of the constructor class methods""" @@ -147,9 +150,15 @@ def get_object(cls, endpoint: pf.Platform, login: str) -> User: raise exceptions.ObjectNotFound(login, f"User '{login}' not found") @classmethod - def _api_for(cls, op: str, endpoint: object) -> Optional[str]: + def api_for(cls, op: str, endpoint: object) -> Optional[str]: """Returns the API for a given operation depedning on the SonarQube version""" - return cls.API[op] if endpoint.version() >= (10, 4, 0) else cls.API_V1[op] + if endpoint.is_sonarcloud(): + api_to_use = User.API_SC + elif endpoint.version() < (10, 4, 0): + api_to_use = User.API_V1 + else: + api_to_use = User.API + return api_to_use[op] if op in api_to_use else api_to_use[c.LIST] def __str__(self) -> str: """ @@ -203,7 +212,7 @@ def refresh(self) -> User: :return: The user itself """ - data = json.loads(self.get(User._api_for(c.SEARCH, self.endpoint), params={"q": self.login}).text) + data = json.loads(self.get(User.api_for(c.SEARCH, self.endpoint), params={"q": self.login}).text) for d in data["users"]: if d["login"] == self.login: self.__load(d) @@ -325,7 +334,10 @@ def delete(self) -> bool: def api_params(self, op: str = c.GET) -> types.ApiParams: """Return params used to search/create/delete for that object""" - ops = {c.GET: {"login": self.login}} + if self.endpoint.version() >= (10, 4, 0): + ops = {c.GET: {}} + else: + ops = {c.GET: {"login": self.login}} return ops[op] if op in ops else ops[c.GET] def set_groups(self, group_list: list[str]) -> bool: @@ -368,7 +380,12 @@ def set_scm_accounts(self, accounts_list: list[str]) -> bool: :rtype: bool """ log.debug("Setting SCM accounts of %s to '%s'", str(self), str(accounts_list)) - r = self.post(User.API[c.UPDATE], params={**self.api_params(c.UPDATE), "scmAccount": ",".join(set(accounts_list))}) + if self.endpoint.version() >= (10, 4, 0): + r = self.patch(f"{User.api_for(c.UPDATE, self.endpoint)}/{self.id}", params={"scmAccounts": accounts_list}) + else: + params = self.api_params() + params["scmAccount"] = ",".join(set(accounts_list)) + r = self.post(User.api_for(c.UPDATE, self.endpoint), params=params) if not r.ok: self.scm_accounts = [] return False From d5ea0d103efd055fd9475de122cf5f4c2a13d8f1 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Tue, 24 Dec 2024 15:17:06 +0100 Subject: [PATCH 10/22] Fix double logging for IT tests (#1541) * Fixes double logging * More consistent IT logs --- test/it-tools.sh | 3 +-- test/it.sh | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/it-tools.sh b/test/it-tools.sh index cfe6e3faa..72e363d52 100644 --- a/test/it-tools.sh +++ b/test/it-tools.sh @@ -37,11 +37,10 @@ function logmsg { function run_test { file=$1; shift announced_args=$(get_announced_args $@) - announce_test "$announced_args" + announce_test "$announced_args -f $file" if [ "$1" != "docker" ]; then file="$REPO_ROOT/tmp/$file" fi - announce_test "$announced_args -f $file" if [ "$SONAR_HOST_URL" == "$SONAR_HOST_URL_SONARCLOUD" ]; then "$@" -o okorach -f "$file" 2>>$IT_LOG_FILE else diff --git a/test/it.sh b/test/it.sh index 7f310a9cd..ddb3399b2 100755 --- a/test/it.sh +++ b/test/it.sh @@ -101,7 +101,7 @@ do export SONAR_HOST_URL="http://localhost:$sqport" fi - logmsg "=====> IT $env sonar-measures-export" + logmsg "=====> IT sonar-measures-export $env" f="measures-$env-unrel.csv"; run_test "$f" sonar-measures-export -b -m _main --withURL f="measures-$env-2.csv"; run_test_stdout "$f" sonar-measures-export -b -m _main --withURL @@ -115,29 +115,30 @@ do f="measures-history-$env-2.csv"; run_test "$f" sonar-measures-export -b -k okorach_sonar-tools --history --asTable f="measures-history-$env-3.json"; run_test "$f" sonar-measures-export -b --history - logmsg "=====> IT $env sonar-findings-export" + logmsg "=====> IT sonar-findings-export $env" f="findings-$env-unrel.csv"; run_test "$f" sonar-findings-export -v DEBUG f="findings-$env-1.json"; run_test "$f" sonar-findings-export f="findings-$env-2.json"; run_test_stdout "$f" sonar-findings-export -v DEBUG --format json -k okorach_audio-video-tools,okorach_sonar-tools f="findings-$env-3.json"; run_test_stdout "$f" sonar-findings-export -v DEBUG --format json -k okorach_audio-video-tools,okorach_sonar-tools --useFindings f="findings-$env-4.csv"; run_test_stdout "$f" sonar-findings-export --format csv -k okorach_audio-video-tools,okorach_sonar-tools --csvSeparator '+' - + + if [ "$env" = "sonarcloud" ]; then logmsg "IT $env sonar-audit SKIPPED" logmsg "IT $env sonar-housekeeper SKIPPED" else - logmsg "=====> IT $env sonar-audit" + logmsg "=====> IT sonar-audit $env" f="audit-$env-unrel.csv"; run_test_stdout "$f" sonar-audit f="audit-$env-1.json"; run_test "$f" sonar-audit f="audit-$env-2.json"; run_test_stdout "$f" sonar-audit --format json --what qualitygates,qualityprofiles,settings f="audit-$env-3.csv"; run_test_stdout "$f" sonar-audit --csvSeparator '+' --format csv - logmsg "=====> IT $env sonar-housekeeper" + logmsg "=====> IT sonar-housekeeper $env" f="housekeeper-$env-1.csv"; run_test_stdout "$f" sonar-housekeeper -P 365 -B 90 -T 180 -R 30 fi - logmsg "=====> IT $env sonar-loc" + logmsg "=====> IT sonar-loc $env" f="loc-$env-1.csv"; run_test_stdout "$f" sonar-loc f="loc-$env-unrel.csv"; run_test_stdout "$f" sonar-loc -n -a f="loc-$env-2.csv"; run_test "$f" sonar-loc -n -a --csvSeparator ';' @@ -163,7 +164,7 @@ do f="proj-export-$env-2.json"; run_test "$f" sonar-projects-export fi - logmsg "=====> sonar-findings-export $env ADMIN export" + logmsg "=====> IT sonar-findings-export $env ADMIN export" f1="findings-$env-admin.csv"; run_test "$f1" sonar-findings-export -v DEBUG -k okorach_audio-video-tools,okorach_sonar-tools #-------------------------------------------------------------------------- @@ -172,7 +173,7 @@ do if [ "$env" = "sonarcloud" ]; then logmsg "sonar-projects-export $env SKIPPED" else - logmsg "=====> sonar-findings-export $env USER export" + logmsg "=====> IT sonar-findings-export $env USER export" export SONAR_TOKEN=$SONAR_TOKEN_USER_USER f2="findings-$env-user.csv"; run_test "$f2" sonar-findings-export -v DEBUG -k okorach_audio-video-tools,okorach_sonar-tools fi From ec2fad899eb8a65af7c5e0926b156b6e1244a7b7 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 25 Dec 2024 18:14:44 +0100 Subject: [PATCH 11/22] Compatibility LTS (#1543) * Pass api_version=1 for search users below 10.4 * Pass special USER token for LTS 9.9 * Export/Import AI code assurance only for 10.7+ and commercial editions * Add failed webhooks deliveries * DOn't log BODY when empty * Fix test on taskID size for 9.9 * Add copy() * Improve tests on project QG * Fix nrb of rules on 9,9 * Adapt tests to 9.9 * Compat API 9.9 * Formatting * Adapt tests to 9.9 --- doc/sonar-audit.md | 2 ++ sonar/platform.py | 2 +- sonar/projects.py | 7 ++++--- sonar/qualitygates.py | 5 +++++ sonar/users.py | 11 +++++++---- test/conftest.py | 15 ++++++++++++++- test/it.sh | 14 ++++++++++---- test/test_devops.py | 5 ++++- test/test_issues.py | 33 +++++++++++++++++++-------------- test/test_migration.py | 8 +++----- test/test_projects.py | 34 +++++++++++++++++++--------------- test/test_qp.py | 4 ++-- test/test_tasks.py | 12 ++++++++---- 13 files changed, 98 insertions(+), 54 deletions(-) diff --git a/doc/sonar-audit.md b/doc/sonar-audit.md index e45556e9d..f889f89fb 100644 --- a/doc/sonar-audit.md +++ b/doc/sonar-audit.md @@ -214,6 +214,8 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';' - Tokens without expiration date - Groups: (if `audit.groups = yes`, default `yes`) - Empty groups +- WebHooks: + - Failed webhooks deliveries # License diff --git a/sonar/platform.py b/sonar/platform.py index 3078549bf..fdfd02c42 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -401,7 +401,7 @@ def __urlstring(self, api: str, params: types.ApiParams, data: str = None) -> st params_string = "&".join([f"{k}={requests.utils.quote(str(v))}" for k, v in good_params.items()]) if len(params_string) > 0: url += f"?{params_string}" - if data is not None: + if data is not None and len(data) > 0: url += f" - BODY: {data}" return url diff --git a/sonar/projects.py b/sonar/projects.py index 69e432abb..192956355 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -1079,9 +1079,10 @@ def export(self, export_settings: types.ConfigSettings, settings_list: dict[str, settings_dict = settings.get_bulk(endpoint=self.endpoint, component=self, settings_list=settings_list, include_not_set=False) # json_data.update({s.to_json() for s in settings_dict.values() if include_inherited or not s.inherited}) - ai = self.get_ai_code_assurance() - contains_ai = ai is not None and ai != "NONE" - json_data[_CONTAINS_AI_CODE] = contains_ai + if self.endpoint.version() >= (10, 7, 0) and self.endpoint.edition() != "community": + ai = self.get_ai_code_assurance() + contains_ai = ai is not None and ai != "NONE" + json_data[_CONTAINS_AI_CODE] = contains_ai for s in settings_dict.values(): if not export_settings.get("INCLUDE_INHERITED", False) and s.inherited: continue diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 031153325..b9acb8a61 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -257,6 +257,11 @@ def set_permissions(self, permissions_list: types.ObjectJsonRepr) -> QualityGate """ return self.permissions().set(permissions_list) + def copy(self, new_qg_name: str) -> QualityGate: + """Copies the QG into another one with name new_qg_name""" + data = json.loads(self.post("qualitygates/copy", params={"id": self.key, "name": new_qg_name}).text) + return QualityGate(self.endpoint, name=new_qg_name, data=data) + def set_as_default(self) -> bool: """Sets the quality gate as the default :return: Whether setting as default quality gate was successful diff --git a/sonar/users.py b/sonar/users.py index 0266c58be..466345ccf 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -124,7 +124,7 @@ def create(cls, endpoint: pf.Platform, login: str, name: str, is_local: bool = T if is_local: params["password"] = password if password else login try: - endpoint.post(User.API[c.CREATE], params=params) + endpoint.post(User.api_for(c.CREATE, endpoint), params=params) except (ConnectionError, RequestException) as e: util.handle_error(e, f"creating user '{login}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) raise exceptions.ObjectAlreadyExists(login, util.sonar_error(e.response)) @@ -188,7 +188,7 @@ def __load(self, data: types.ApiPayload) -> None: self.last_login = max(dt1, dt2) self.id = data["id"] self.__tokens = None - self._groups = self.groups(data) #: User groups + self._groups = self.groups(data) self.sq_json = data def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: @@ -200,7 +200,9 @@ def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: data = json.loads(self.get(_GROUPS_API_SC, self.api_params(c.GET)).text)["groups"] self._groups = [g["name"] for g in data] elif self.endpoint.version() < (10, 4, 0): - self._groups = data.get("groups", []) #: User groups + if data is None: + data = self.sq_json + self._groups = data.get("groups", []) else: data = json.loads(self.get(User.API["GROUP_MEMBERSHIPS"], {"userId": self.id, "pageSize": 500}).text)["groupMemberships"] log.debug("Groups = %s", str(data)) @@ -448,7 +450,8 @@ def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, U :rtype: dict{login: User} """ log.debug("Searching users with params %s", str(params)) - return dict(sorted(sqobject.search_objects(endpoint=endpoint, object_class=User, params=params, api_version=2).items())) + api_version = 2 if endpoint.version() >= (10, 4, 0) else 1 + return dict(sorted(sqobject.search_objects(endpoint=endpoint, object_class=User, params=params, api_version=api_version).items())) def get_list(endpoint: pf.Platform) -> dict[str, User]: diff --git a/test/conftest.py b/test/conftest.py index 70b07a149..c93091420 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -26,7 +26,7 @@ import pytest import utilities as util -from sonar import projects, applications, portfolios, qualityprofiles, exceptions, logging, issues, users, groups +from sonar import projects, applications, portfolios, qualityprofiles, qualitygates, exceptions, logging, issues, users, groups TEMP_FILE_ROOT = f"temp.{os.getpid()}" CSV_FILE = f"{TEMP_FILE_ROOT}.csv" @@ -215,6 +215,19 @@ def get_test_application() -> Generator[applications.Application]: pass +@pytest.fixture +def get_test_quality_gate() -> Generator[qualitygates.QualityGate]: + """setup of tests""" + util.start_logging() + sonar_way = qualitygates.QualityGate.get_object(util.SQ, "Sonar way") + o = sonar_way.copy(util.TEMP_KEY) + yield o + try: + o.delete() + except exceptions.ObjectNotFound: + pass + + @pytest.fixture def get_60_groups() -> Generator[list[groups.Group]]: util.start_logging() diff --git a/test/it.sh b/test/it.sh index ddb3399b2..657721f5d 100755 --- a/test/it.sh +++ b/test/it.sh @@ -24,6 +24,7 @@ DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" source "$DIR/it-tools.sh" DB_BACKUPS_DIR=~/backup +IT_TEST_PORT=9888 function backup_for { case $1 in @@ -94,9 +95,10 @@ do else id="it$$" logmsg "Creating IT test environment $env - sonarId $id" - sqport=10020 + sqport=$IT_TEST_PORT + pgport=$(expr $sqport - 4000) # echo sonar create -i $id -t "$(tag_for "$env")" -s $sqport -p 6020 -f "$(backup_for "$env")" - sonar create -i $id -t "$(tag_for "$env")" -s $sqport -p 6020 -f "$(backup_for "$env")" 1>$IT_LOG_FILE 2>&1 + sonar create -i $id -t "$(tag_for "$env")" -s $sqport -p $pgport -f "$(backup_for "$env")" 1>$IT_LOG_FILE 2>&1 export SONAR_TOKEN=$SONAR_TOKEN_ADMIN_USER export SONAR_HOST_URL="http://localhost:$sqport" fi @@ -174,7 +176,11 @@ do logmsg "sonar-projects-export $env SKIPPED" else logmsg "=====> IT sonar-findings-export $env USER export" - export SONAR_TOKEN=$SONAR_TOKEN_USER_USER + if [ "$env" = "lts" ]; then + export SONAR_TOKEN=$SONAR_TOKEN_USER_USER_LTS + else + export SONAR_TOKEN=$SONAR_TOKEN_USER_USER + fi f2="findings-$env-user.csv"; run_test "$f2" sonar-findings-export -v DEBUG -k okorach_audio-video-tools,okorach_sonar-tools fi @@ -211,7 +217,7 @@ do if [ "$env" != "sonarcloud" ]; then logmsg "Deleting environment sonarId $id" - sonar delete -i "$id" 1>$IT_LOG_FILE 2>&1 + # sonar delete -i "$id" 1>$IT_LOG_FILE 2>&1 fi done diff --git a/test/test_devops.py b/test/test_devops.py index ac0c6e59e..8d531a8d7 100644 --- a/test/test_devops.py +++ b/test/test_devops.py @@ -42,7 +42,10 @@ def test_get_object_gh() -> None: """test_get_object_gh""" plt = devops.get_object(endpoint=util.SQ, key=GH_KEY) assert plt.url == "https://api.github.com" - assert plt._specific["appId"] == "946159" + if util.SQ.version() >= (10, 0, 0): + assert plt._specific["appId"] == "946159" + else: + assert plt._specific["appId"] == "1096234" assert str(plt) == f"devops platform '{GH_KEY}'" diff --git a/test/test_issues.py b/test/test_issues.py index 267f698e8..bb47fb952 100644 --- a/test/test_issues.py +++ b/test/test_issues.py @@ -28,15 +28,19 @@ from sonar.util import constants as c -ISSUE_WITH_CHANGELOG = "402452b7-fd3a-4487-97cc-1c996697b397" -ISSUE_2 = "a1fddba4-9e70-46c6-ac95-e815104ead59" +ISSUE_FP = "402452b7-fd3a-4487-97cc-1c996697b397" +ISSUE_FP_V9_9 = "AZNT89kklhFmauJ_HQSK" +ISSUE_ACCEPTED = "a1fddba4-9e70-46c6-ac95-e815104ead59" +ISSUE_ACCEPTED_V9_9 = "AZI6frkTuTfDeRt_hspx" def test_issue() -> None: """Test issues""" + issue_key = ISSUE_FP if tutil.SQ.version() >= (10, 0, 0) else ISSUE_FP_V9_9 + issue_key_accepted = ISSUE_ACCEPTED if tutil.SQ.version() >= (10, 0, 0) else ISSUE_ACCEPTED_V9_9 issues_d = issues.search_by_project(endpoint=tutil.SQ, project_key=tutil.LIVE_PROJECT) - assert ISSUE_WITH_CHANGELOG in issues_d - issue = issues_d[ISSUE_WITH_CHANGELOG] + + issue = issues_d[issue_key] assert not issue.is_security_issue() assert not issue.is_hotspot() assert not issue.is_accepted() @@ -47,12 +51,12 @@ def test_issue() -> None: assert not issue.is_wont_fix() assert issue.is_false_positive() assert issue.refresh() - assert issue.api_params(c.LIST) == {"issues": ISSUE_WITH_CHANGELOG} - assert issue.api_params(c.SET_TAGS) == {"issue": ISSUE_WITH_CHANGELOG} - assert issue.api_params(c.GET_TAGS) == {"issues": ISSUE_WITH_CHANGELOG} + assert issue.api_params(c.LIST) == {"issues": issue_key} + assert issue.api_params(c.SET_TAGS) == {"issue": issue_key} + assert issue.api_params(c.GET_TAGS) == {"issues": issue_key} - assert ISSUE_2 in issues_d - issue2 = issues_d[ISSUE_2] + assert issue_key_accepted in issues_d + issue2 = issues_d[issue_key_accepted] assert not issue.almost_identical_to(issue2) @@ -124,10 +128,11 @@ def test_assign() -> None: def test_changelog() -> None: """Test changelog""" issues_d = issues.search_by_project(endpoint=tutil.SQ, project_key=tutil.LIVE_PROJECT) - assert ISSUE_WITH_CHANGELOG in issues_d - issue = issues_d[ISSUE_WITH_CHANGELOG] - assert issue.key == ISSUE_WITH_CHANGELOG - assert str(issue) == f"Issue key '{ISSUE_WITH_CHANGELOG}'" + issue_key = ISSUE_FP if tutil.SQ.version() >= (10, 0, 0) else ISSUE_FP_V9_9 + assert issue_key in issues_d + issue = issues_d[issue_key] + assert issue.key == issue_key + assert str(issue) == f"Issue key '{issue_key}'" assert issue.is_false_positive() changelog_l = list(issue.changelog().values()) assert len(changelog_l) == 1 @@ -150,7 +155,7 @@ def test_changelog() -> None: assert not changelog.is_assignment() assert changelog.new_assignee() is None assert changelog.old_assignee() is None - assert datetime(2024, 10, 20) <= util.string_to_date(changelog.date()).replace(tzinfo=None) < datetime(2024, 10, 22) + assert datetime(2024, 10, 20) <= util.string_to_date(changelog.date()).replace(tzinfo=None) < datetime(2024, 12, 26) assert changelog.author() == "admin" assert not changelog.is_tag() assert changelog.get_tags() is None diff --git a/test/test_migration.py b/test/test_migration.py index 8bbe7cb62..a55c7a65f 100644 --- a/test/test_migration.py +++ b/test/test_migration.py @@ -77,17 +77,15 @@ def test_migration(get_json_file: Generator[str]) -> None: assert json_config["users"]["olivier"]["externalProvider"] == "sonarqube" GH_USER = "olivier-korach65532" - GL_USER = "olivier-korach22656" + GL_USER = "olivier-korach22656" if util.SQ.version() > (10, 0, 0) else "olivier-korach82556" USER = GL_USER u = json_config["users"][USER] assert u["name"] == "Olivier Korach" assert not u["local"] + assert u["externalProvider"] == ("gitlab" if USER == GL_USER else "github") if util.SQ.version() >= (10, 0, 0): - assert u["externalProvider"] == ("gitlab" if USER == GL_USER else "github") assert u["externalLogin"] == "okorach" assert u["email"] == "olivier.korach@gmail.com" - else: - assert u["externalProvider"] == "sonarqube" p = json_config["projects"]["okorach_sonar-tools"] assert "lastTaskScannerContext" in p["backgroundTasks"] @@ -109,11 +107,11 @@ def test_migration(get_json_file: Generator[str]) -> None: assert p["branches"]["master"]["hotspots"]["acknowledged"] == 0 p = json_config["projects"]["checkstyle-issues"] - assert len(p["branches"]["main"]["issues"]["thirdParty"]) > 0 if util.SQ.version() >= (10, 0, 0): assert json_config["projects"]["demo:gitlab-ci-maven"]["detectedCi"] == "Gitlab CI" assert json_config["projects"]["demo:github-actions-cli"]["detectedCi"] == "Github Actions" + assert len(p["branches"]["main"]["issues"]["thirdParty"]) > 0 for p in json_config["portfolios"].values(): assert "projects" in p diff --git a/test/test_projects.py b/test/test_projects.py index 495620ead..424dc9c9f 100644 --- a/test/test_projects.py +++ b/test/test_projects.py @@ -25,7 +25,7 @@ import pytest -from sonar import projects, exceptions, qualityprofiles +from sonar import projects, exceptions, qualityprofiles, qualitygates from sonar.audit import config import utilities as util @@ -102,7 +102,8 @@ def test_get_findings() -> None: def test_count_third_party_issues() -> None: """test_count_third_party_issues""" proj = projects.Project.get_object(endpoint=util.SQ, key="checkstyle-issues") - assert len(proj.count_third_party_issues(filters={"branch": "develop"})) > 0 + if util.SQ.version() >= (10, 0, 0): + assert len(proj.count_third_party_issues(filters={"branch": "develop"})) > 0 assert len(proj.count_third_party_issues(filters={"branch": "non-existing-branch"})) == 0 @@ -135,6 +136,7 @@ def test_already_exists() -> None: def test_binding() -> None: """test_binding""" + util.start_logging() proj = projects.Project.get_object(util.SQ, util.TEST_KEY) assert proj.has_binding() assert proj.binding() is not None @@ -183,29 +185,31 @@ def test_set_tags(get_test_project: callable) -> None: assert not proj.set_tags(None) -def test_set_quality_gate(get_test_project: callable) -> None: +def test_set_quality_gate(get_test_project: Generator[projects.Project], get_test_quality_gate: Generator[qualitygates.QualityGate]) -> None: """test_set_quality_gate""" proj = get_test_project - assert proj.set_quality_gate(util.EXISTING_QG) + qg = get_test_quality_gate + assert proj.set_quality_gate(qg.name) assert not proj.set_quality_gate(None) assert not proj.set_quality_gate(util.NON_EXISTING_KEY) proj.key = util.NON_EXISTING_KEY - assert not proj.set_quality_gate(util.EXISTING_QG) + assert not proj.set_quality_gate(qg.name) def test_ai_code_assurance(get_test_project: Generator[projects.Project]) -> None: """test_set_ai_code_assurance""" - proj = get_test_project - assert proj.set_contains_ai_code(True) - assert proj.get_ai_code_assurance() in ("CONTAINS_AI_CODE", "AI_CODE_ASSURED") - assert proj.set_contains_ai_code(False) - assert proj.get_ai_code_assurance() == "NONE" - proj.key = util.NON_EXISTING_KEY - assert not proj.set_contains_ai_code(True) - assert proj.get_ai_code_assurance() is None - assert not proj.set_contains_ai_code(False) - assert proj.get_ai_code_assurance() is None + if util.SQ.version() >= (10, 7, 0): + proj = get_test_project + assert proj.set_contains_ai_code(True) + assert proj.get_ai_code_assurance() in ("CONTAINS_AI_CODE", "AI_CODE_ASSURED") + assert proj.set_contains_ai_code(False) + assert proj.get_ai_code_assurance() == "NONE" + proj.key = util.NON_EXISTING_KEY + assert not proj.set_contains_ai_code(True) + assert proj.get_ai_code_assurance() is None + assert not proj.set_contains_ai_code(False) + assert proj.get_ai_code_assurance() is None def test_set_quality_profile(get_test_project: Generator[projects.Project], get_test_qp: Generator[qualityprofiles.QualityProfile]) -> None: diff --git a/test/test_qp.py b/test/test_qp.py index 7b7c226f7..a6dc38300 100644 --- a/test/test_qp.py +++ b/test/test_qp.py @@ -128,7 +128,7 @@ def test_export() -> None: def test_add_remove_rules(get_test_qp: Generator[qualityprofiles.QualityProfile]) -> None: """test_add_remove_rules""" qp = get_test_qp - RULE1, RULE2, RULE3 = "python:S6542", "python:FunctionComplexity", "python:S139" + RULE1, RULE2, RULE3 = "python:S1142", "python:FunctionComplexity", "python:S139" ruleset = {RULE1: "MAJOR", RULE2: "MAJOR"} qp.activate_rules(ruleset) qp_rules = qp.rules() @@ -148,7 +148,7 @@ def test_add_remove_rules(get_test_qp: Generator[qualityprofiles.QualityProfile] assert qp.set_parent("Sonar way") rulecount = len(qp.rules()) - assert rulecount > 250 + assert rulecount > 250 if util.SQ.version() >= (10, 0, 0) else 200 assert qp.deactivate_rule(RULE3) assert len(qp.rules()) == rulecount - 1 diff --git a/test/test_tasks.py b/test/test_tasks.py index 5040a2eed..3d20e7e19 100644 --- a/test/test_tasks.py +++ b/test/test_tasks.py @@ -22,8 +22,7 @@ """ Test of the tasks module and class """ import utilities as tutil -from sonar import tasks, logging -from sonar import utilities as util +from sonar import tasks def test_task() -> None: @@ -34,7 +33,10 @@ def test_task() -> None: task.sq_json = None task._load() assert task.sq_json is not None - assert len(task.id()) == 36 + if tutil.SQ.version() >= (10, 0, 0): + assert len(task.id()) == 36 + else: + assert len(task.id()) == 20 assert task.status() == tasks.SUCCESS assert 100 <= task.execution_time() <= 100000 assert task.submitter() == "admin" @@ -60,8 +62,10 @@ def test_audit() -> None: def test_no_scanner_context() -> None: """test_no_scanner_context""" + tutil.start_logging() task = tasks.search_last(component_key="project1", endpoint=tutil.SQ, type="REPORT") - assert task.scanner_context() is None + if tutil.SQ.version() >= (10, 0, 0): + assert task.scanner_context() is None settings = {} task.audit(settings) From 471564153bd4da42e32638f39d84cfa6640c917b Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 25 Dec 2024 18:35:36 +0100 Subject: [PATCH 12/22] Fix (#1544) --- sonar/qualitygates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index b9acb8a61..aa71a4fe0 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -259,7 +259,7 @@ def set_permissions(self, permissions_list: types.ObjectJsonRepr) -> QualityGate def copy(self, new_qg_name: str) -> QualityGate: """Copies the QG into another one with name new_qg_name""" - data = json.loads(self.post("qualitygates/copy", params={"id": self.key, "name": new_qg_name}).text) + data = json.loads(self.post("qualitygates/copy", params={"sourceName": self.name, "name": new_qg_name}).text) return QualityGate(self.endpoint, name=new_qg_name, data=data) def set_as_default(self) -> bool: From fce5940fd9f63e02d862f09e04b0e9fae90524e2 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 25 Dec 2024 20:14:58 +0100 Subject: [PATCH 13/22] Fixes #1542 (#1546) --- sonar/platform.py | 6 +++++- sonar/projects.py | 17 +++++++++++++++++ sonar/settings.py | 5 +++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sonar/platform.py b/sonar/platform.py index fdfd02c42..723310cd2 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -361,7 +361,11 @@ def get_settings(self, settings_list: list[str] = None) -> dict[str, any]: def __settings(self, settings_list: types.KeyList = None, include_not_set: bool = False) -> dict[str, settings.Setting]: log.info("getting global settings") - return settings.get_bulk(endpoint=self, settings_list=settings_list, include_not_set=include_not_set) + settings_dict = settings.get_bulk(endpoint=self, settings_list=settings_list, include_not_set=include_not_set) + ai_code_fix = settings.Setting.read(endpoint=self, key=settings.AI_CODE_FIX) + if ai_code_fix: + settings_dict[ai_code_fix.key] = ai_code_fix + return settings_dict def get_setting(self, key: str) -> any: """Returns a platform global setting value from its key diff --git a/sonar/projects.py b/sonar/projects.py index 192956355..b5cf36b9d 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -630,6 +630,22 @@ def revision(self) -> str: self.ci() return self._revision + def ai_code_fix(self) -> Optional[str]: + """Returns whether this porject is enabled for AI Code Fix (if only enabled per project)""" + log.debug("Getting project AI Code Fix suggestion flag") + global_setting = settings.Setting.read(key=settings.AI_CODE_FIX, endpoint=self.endpoint) + log.debug("Global Setting = %s JSON = %s", str(global_setting.value), util.json_dump(self.sq_json)) + if not global_setting or global_setting.value != "ENABLED_FOR_SOME_PROJECTS": + return None + if "isAiCodeFixEnabled" not in self.sq_json: + r = self.get("components/search_projects") + data = json.loads(r.text) + p_data = next((p for p in data["components"] if p["key"] == self.key), None) + if p_data: + self.sq_json.update(p_data) + log.debug("RETURNING = %s", str(self.sq_json.get("isAiCodeFixEnabled", None))) + return self.sq_json.get("isAiCodeFixEnabled", None) + def __audit_scanner(self, audit_settings: types.ConfigSettings) -> list[Problem]: if audit_settings.get(AUDIT_MODE_PARAM, "") == "housekeeper": return [] @@ -1058,6 +1074,7 @@ def export(self, export_settings: types.ConfigSettings, settings_list: dict[str, json_data["qualityProfiles"] = self.__export_get_qp() json_data["links"] = self.links() json_data["permissions"] = self.permissions().to_json(csv=export_settings.get("INLINE_LISTS", True)) + json_data["aiCodeFix"] = self.ai_code_fix() json_data["branches"] = self.__get_branch_export(export_settings) json_data["tags"] = self.get_tags() json_data["visibility"] = self.visibility() diff --git a/sonar/settings.py b/sonar/settings.py index 93825f8f5..9db5433a8 100644 --- a/sonar/settings.py +++ b/sonar/settings.py @@ -59,6 +59,7 @@ NEW_CODE_PERIOD = "newCodePeriod" COMPONENT_VISIBILITY = "visibility" PROJECT_DEFAULT_VISIBILITY = "projects.default.visibility" +AI_CODE_FIX = "sonar.ai.suggestions.enabled" DEFAULT_BRANCH = "-DEFAULT_BRANCH-" @@ -292,7 +293,7 @@ def is_global(self) -> bool: if self.component: return False if self._is_global is None: - self._is_global = self.definition() is not None + self._is_global = self.definition() is not None or self.key == AI_CODE_FIX return self._is_global def is_internal(self) -> bool: @@ -354,7 +355,7 @@ def category(self) -> tuple[str, str]: if m: return (AUTH_SETTINGS, None) if self.key not in (NEW_CODE_PERIOD, PROJECT_DEFAULT_VISIBILITY, COMPONENT_VISIBILITY) and not re.match( - r"^(email|sonar\.core|sonar\.allowPermission|sonar\.builtInQualityProfiles|sonar\.core|" + r"^(email|sonar\.core|sonar\.allowPermission|sonar\.builtInQualityProfiles|sonar\.ai|" r"sonar\.cpd|sonar\.dbcleaner|sonar\.developerAggregatedInfo|sonar\.governance|sonar\.issues|sonar\.lf|sonar\.notifications|" r"sonar\.portfolios|sonar\.qualitygate|sonar\.scm\.disabled|sonar\.scm\.provider|sonar\.technicalDebt|sonar\.validateWebhooks|" r"sonar\.docker|sonar\.login|sonar\.kubernetes|sonar\.plugins|sonar\.documentation|sonar\.projectCreation|" From 92aa5b03a5feb27a1fe0ad4ea04ff0f3b074755c Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 25 Dec 2024 21:05:39 +0100 Subject: [PATCH 14/22] Performance-for-ai-code-fix (#1547) * Performance improvement to collect project AI code fix * Improvement * Use paginated api * Removed unused function * Move get_paginated in platform --- sonar/platform.py | 22 ++++++++++++++-------- sonar/projects.py | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/sonar/platform.py b/sonar/platform.py index 723310cd2..4655a46b8 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -268,6 +268,20 @@ def __run_request(self, request: callable, api: str, params: types.ApiParams = N util.handle_error(e, "") return r + def get_paginated(self, api: str, return_field: str, params: types.ApiParams = None) -> types.ObjectJsonRepr: + """Returns all pages of a paginated API""" + new_params = {} if params is None else params.copy() + new_params["ps"] = 500 + new_params["p"] = 1 + data = json.loads(self.get(api, params=new_params).text) + nb_pages = util.nbr_pages(data, api_version=1) + if nb_pages == 1: + return data + for page in range(2, nb_pages + 1): + new_params["p"] = page + data[return_field].update(json.loads(self.get(api, params=new_params).text)[return_field]) + return data + def global_permissions(self) -> dict[str, any]: """Returns the SonarQube platform global permissions @@ -938,11 +952,3 @@ def audit(endpoint: Platform, audit_settings: types.ConfigSettings, **kwargs) -> if "write_q" in kwargs: kwargs["write_q"].put(pbs) return pbs - - -def log_and_exit(exception: Exception) -> None: - """If HTTP response is not OK, display an error log and exit""" - err_code, msg = util.http_error_and_code(exception) - if err_code is None: - return - util.exit_fatal(msg, err_code) diff --git a/sonar/projects.py b/sonar/projects.py index b5cf36b9d..5381f539c 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -77,6 +77,7 @@ "visibility", "qualityGate", "webhooks", + "aiCodeFix", ) _UNNEEDED_CONTEXT_DATA = ( @@ -631,19 +632,16 @@ def revision(self) -> str: return self._revision def ai_code_fix(self) -> Optional[str]: - """Returns whether this porject is enabled for AI Code Fix (if only enabled per project)""" - log.debug("Getting project AI Code Fix suggestion flag") + """Returns whether this project is enabled for AI Code Fix (if only enabled per project)""" + log.debug("Getting project AI Code Fix suggestion flag for %s", str(self)) global_setting = settings.Setting.read(key=settings.AI_CODE_FIX, endpoint=self.endpoint) - log.debug("Global Setting = %s JSON = %s", str(global_setting.value), util.json_dump(self.sq_json)) if not global_setting or global_setting.value != "ENABLED_FOR_SOME_PROJECTS": return None if "isAiCodeFixEnabled" not in self.sq_json: - r = self.get("components/search_projects") - data = json.loads(r.text) + data = self.endpoint.get_paginated(api="components/search_projects", params={"filter": "qualifier=TRK"}, return_field="components") p_data = next((p for p in data["components"] if p["key"] == self.key), None) if p_data: self.sq_json.update(p_data) - log.debug("RETURNING = %s", str(self.sq_json.get("isAiCodeFixEnabled", None))) return self.sq_json.get("isAiCodeFixEnabled", None) def __audit_scanner(self, audit_settings: types.ConfigSettings) -> list[Problem]: @@ -1484,7 +1482,16 @@ def get_list(endpoint: pf.Platform, key_list: types.KeyList = None, use_cache: b with _CLASS_LOCK: if key_list is None or len(key_list) == 0 or not use_cache: log.info("Listing projects") - return dict(sorted(search(endpoint=endpoint).items())) + p_list = dict(sorted(search(endpoint=endpoint).items())) + global_setting = settings.Setting.read(key=settings.AI_CODE_FIX, endpoint=endpoint) + if not global_setting or global_setting.value != "ENABLED_FOR_SOME_PROJECTS": + return p_list + for d in endpoint.get_paginated(api="components/search_projects", params={"filter": "qualifier=TRK"}, return_field="components")[ + "components" + ]: + if d["key"] in p_list: + p_list[d["key"]].sq_json.update(d) + return p_list return {key: Project.get_object(endpoint, key) for key in sorted(key_list)} From 3ffc715944840b1d45aa62423cf81824a561514d Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Thu, 26 Dec 2024 13:08:58 +0100 Subject: [PATCH 15/22] Improve groups coverage (#1548) * Add API v2 format in sonar_error() * Preserve user id, login and name at creation * Add members() and some fixes * Remove api lower since IDs capitalization matters in API v2 * Add get_object_by_id() * Add more tests * Quality pass * Quality pass --- sonar/groups.py | 55 +++++++++++++++----------- sonar/platform.py | 5 +-- sonar/users.py | 23 ++++++++++- sonar/utilities.py | 3 ++ test/conftest.py | 26 +++++++++++-- test/test_groups.py | 95 ++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 176 insertions(+), 31 deletions(-) diff --git a/sonar/groups.py b/sonar/groups.py index 8aa360c58..703966d4f 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -31,7 +31,7 @@ import sonar.platform as pf import sonar.sqobject as sq import sonar.utilities as util -from sonar import exceptions +from sonar import exceptions, users from sonar.audit import rules from sonar.audit.problem import Problem @@ -76,12 +76,12 @@ def __init__(self, endpoint: pf.Platform, name: str, data: types.ApiPayload) -> super().__init__(endpoint=endpoint, key=name) self.name = name #: Group name self.description = data.get("description", "") #: Group description - self.__members_count = data.get("membersCount", None) + self.__members = None self.__is_default = data.get("default", None) self.id = data.get("id", None) #: SonarQube 10.4+ Group id self.sq_json = data Group.CACHE.put(self) - log.debug("Created %s object, id %s", str(self), str(self.id)) + log.debug("Created %s object, id '%s'", str(self), str(self.id)) @classmethod def read(cls, endpoint: pf.Platform, name: str) -> Group: @@ -115,11 +115,13 @@ def create(cls, endpoint: pf.Platform, name: str, description: str = None) -> Gr """ log.debug("Creating group '%s'", name) try: - endpoint.post(Group.api_for(c.CREATE, endpoint), params={"name": name, "description": description}) + data = json.loads(endpoint.post(Group.api_for(c.CREATE, endpoint), params={"name": name, "description": description}).text) except (ConnectionError, RequestException) as e: util.handle_error(e, f"creating group '{name}'", catch_http_errors=(HTTPStatus.BAD_REQUEST,)) raise exceptions.ObjectAlreadyExists(name, util.sonar_error(e.response)) - return cls.read(endpoint=endpoint, name=name) + o = cls.read(endpoint=endpoint, name=name) + o.sq_json.update(data) + return o @classmethod def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> Group: @@ -190,27 +192,36 @@ def __str__(self) -> str: def is_default(self) -> bool: """ :return: whether the group is a default group (sonar-users only for now) or not - :rtype: bool """ return self.__is_default + def members(self, use_cache: bool = True) -> list[users.User]: + """Returns the group members""" + if self.__members is None or not use_cache: + if self.endpoint.version() >= (10, 4, 0): + data = json.loads(self.get(MEMBERSHIP_API, params={"groupId": self.id}).text)["groupMemberships"] + log.debug("MEMBER DATA = %s", util.json_dump(data)) + self.__members = [users.User.get_object_by_id(self.endpoint, d["userId"]) for d in data] + else: + data = self.endpoint.get_paginated("api/user_groups/users", return_field="users", params={"name": self.name}) + self.__members = [users.User.get_object(self.endpoint, d["login"]) for d in data] + return self.__members + def size(self) -> int: """ - :return: Number of users members of the group - :rtype: int + :return: Number of users in the group """ - return self.__members_count + return len(self.members()) def url(self) -> str: """ :return: the SonarQube permalink URL to the group, actually the global groups page only since this is as close as we can get to the precise group definition - :rtype: str """ return f"{self.endpoint.url}/admin/groups" def add_user(self, user: object) -> bool: - """Adds a user in the group + """Adds an user to the group :param user: the User to add :return: Whether the operation succeeded @@ -245,6 +256,7 @@ def remove_user(self, user: object) -> bool: for m in json.loads(self.get(MEMBERSHIP_API, params={"userId": user.id}).text)["groupMemberships"]: if m["groupId"] == self.id: return self.endpoint.delete(f"{Group.api_for(REMOVE_USER, self.endpoint)}/{m['id']}").ok + raise exceptions.ObjectNotFound(user.login, f"{str(self)} or user id '{user.id} not found") else: params = {"login": user.login, "name": self.name} return self.post(Group.api_for(REMOVE_USER, self.endpoint), params=params).ok @@ -267,9 +279,9 @@ def audit(self, audit_settings: types.ConfigSettings = None) -> list[Problem]: :return: List of problems found, or empty list :rtype: list[Problem] """ - log.debug("Auditing %s", str(self)) + log.debug("Auditing %s size %s", str(self), str(self.size())) problems = [] - if audit_settings.get("audit.groups.empty", True) and self.__members_count == 0: + if audit_settings.get("audit.groups.empty", True) and self.size() == 0: problems = [Problem(rules.get_rule(rules.RuleId.GROUP_EMPTY), self, str(self))] return problems @@ -282,7 +294,7 @@ def to_json(self, full_specs: bool = False) -> types.ObjectJsonRepr: :rtype: dict """ if full_specs: - json_data = {self.name: self.sq_json} + json_data = self.sq_json.copy() else: json_data = {"name": self.name} json_data["description"] = self.description if self.description and self.description != "" else None @@ -299,11 +311,10 @@ def set_description(self, description: str) -> bool: """ if description is None or description == self.description: log.debug("No description to update for %s", str(self)) - return True + return False log.debug("Updating %s with description = %s", str(self), description) if self.endpoint.version() >= (10, 4, 0): - data = json.dumps({"description": description}) - r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", data=data) + r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", params={"description": description}) else: r = self.post(Group.API_V1[c.UPDATE], params={"currentName": self.key, "description": description}) if r.ok: @@ -444,15 +455,13 @@ def import_config(endpoint: pf.Platform, config_data: types.ObjectJsonRepr, key_ create_or_update(endpoint, name, desc) -def exists(group_name: str, endpoint: pf.Platform) -> bool: +def exists(endpoint: pf.Platform, name: str) -> bool: """ + :param endpoint: reference to the SonarQube platform :param group_name: group name to check - :type group_name: str - :param Platform endpoint: reference to the SonarQube platform - :return: whether the project exists - :rtype: bool + :return: whether the group exists """ - return Group.get_object(name=group_name, endpoint=endpoint) is not None + return Group.get_object(name=name, endpoint=endpoint) is not None def convert_for_yaml(original_json: types.ObjectJsonRepr) -> types.ObjectJsonRepr: diff --git a/sonar/platform.py b/sonar/platform.py index 4655a46b8..33f49c52f 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -711,10 +711,9 @@ def _audit_lta_latest(self) -> list[Problem]: def _normalize_api(api: str) -> str: """Normalizes an API based on its multiple original forms""" - api = api.lower() - if api.startswith("/api"): + if api.startswith("/api/"): pass - elif api.startswith("api"): + elif api.startswith("api/"): api = "/" + api elif api.startswith("/"): api = "/api" + api diff --git a/sonar/users.py b/sonar/users.py index 466345ccf..c58c04851 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -136,7 +136,7 @@ def get_object(cls, endpoint: pf.Platform, login: str) -> User: :param Platform endpoint: Reference to the SonarQube platform :param str login: User login - :raise ObjectNotFound: if login not found + :raises ObjectNotFound: if login not found :return: The user object :rtype: User """ @@ -149,6 +149,27 @@ def get_object(cls, endpoint: pf.Platform, login: str) -> User: return o raise exceptions.ObjectNotFound(login, f"User '{login}' not found") + @classmethod + def get_object_by_id(cls, endpoint: pf.Platform, id: str) -> User: + """Searches a user by its (API v2) id in SonarQube + + :param endpoint: Reference to the SonarQube platform + :param id: User id + :raises ObjectNotFound: if id not found + :raises UnsuppoertedOperation: If SonarQube version < 10.4 + :return: The user object + :rtype: User + """ + if endpoint.version() < (10, 4, 0): + raise exceptions.UnsupportedOperation("Get by ID is an APIv2 features, staring from SonarQube 10.4") + log.debug("Getting user id '%s'", id) + try: + data = json.loads(endpoint.get(f"/api/v2/users-management/users/{id}", mute=()).text) + return cls.load(endpoint, data) + except (ConnectionError, RequestException) as e: + util.handle_error(e, f"getting user id '{id}'", catch_http_errors=(HTTPStatus.NOT_FOUND,)) + raise exceptions.ObjectNotFound(id, f"User id '{id}' not found") + @classmethod def api_for(cls, op: str, endpoint: object) -> Optional[str]: """Returns the API for a given operation depedning on the SonarQube version""" diff --git a/sonar/utilities.py b/sonar/utilities.py index c1131c7a5..c9c3a52cb 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -458,6 +458,9 @@ def sonar_error(response: requests.models.Response) -> str: json_res = json.loads(response.text) if "errors" in json_res: return " | ".join([e["msg"] for e in json.loads(response.text)["errors"]]) + elif "message" in json_res: + # API v2 format + return json_res["message"] else: log.debug("No error found in Response %s", json_dump(json_res)) except json.decoder.JSONDecodeError: diff --git a/test/conftest.py b/test/conftest.py index c93091420..8e7bba35c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -139,15 +139,20 @@ def get_test_issue() -> Generator[issues.Issue]: @pytest.fixture def get_test_user() -> Generator[users.User]: """setup of tests""" - logging.set_logger(util.TEST_LOGFILE) - logging.set_debug_level("DEBUG") + util.start_logging() try: o = users.User.get_object(endpoint=util.SQ, login=util.TEMP_KEY) except exceptions.ObjectNotFound: o = users.User.create(endpoint=util.SQ, login=util.TEMP_KEY, name=f"User name {util.TEMP_KEY}") + (uid, uname, ulogin) = (o.name, o.id, o.login) _ = [o.remove_from_group(g) for g in o.groups() if g != "sonar-users"] yield o - _ = [o.remove_from_group(g) for g in o.groups() if g != "sonar-users"] + try: + (o.name, o.id, o.login) = (uid, uname, ulogin) + _ = [o.remove_from_group(g) for g in o.groups() if g != "sonar-users"] + o.delete() + except exceptions.ObjectNotFound: + pass def rm(file: str) -> None: @@ -228,6 +233,21 @@ def get_test_quality_gate() -> Generator[qualitygates.QualityGate]: pass +@pytest.fixture +def get_test_group() -> Generator[groups.Group]: + """setup of tests""" + util.start_logging() + try: + o = groups.Group.get_object(endpoint=util.SQ, name=util.TEMP_KEY) + except exceptions.ObjectNotFound: + o = groups.Group.create(endpoint=util.SQ, name=util.TEMP_KEY) + yield o + try: + o.delete() + except exceptions.ObjectNotFound: + pass + + @pytest.fixture def get_60_groups() -> Generator[list[groups.Group]]: util.start_logging() diff --git a/test/test_groups.py b/test/test_groups.py index a2537a092..a6cd29eb4 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -27,7 +27,7 @@ import utilities as util from sonar import exceptions -from sonar import groups +from sonar import groups, users GROUP1 = "sonar-users" GROUP2 = "sonar-administrators" @@ -54,3 +54,96 @@ def test_more_than_50_groups(get_60_groups: Generator[list[groups.Group]]) -> No new_group_list = groups.get_list(util.SQ) assert len(new_group_list) > 60 assert set(new_group_list.keys()) > set(g.name for g in group_list) + + +def test_read_non_existing() -> None: + with pytest.raises(exceptions.ObjectNotFound): + groups.Group.read(endpoint=util.SQ, name=util.NON_EXISTING_KEY) + + +def test_create_already_exists(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + with pytest.raises(exceptions.ObjectAlreadyExists): + groups.Group.create(endpoint=util.SQ, name=gr.name) + + +def test_size() -> None: + gr = groups.Group.get_object(endpoint=util.SQ, name="sonar-users") + assert gr.size() > 4 + + +def test_url() -> None: + gr = groups.Group.get_object(endpoint=util.SQ, name="sonar-users") + assert gr.url() == f"{util.SQ.url}/admin/groups" + + +def test_add_non_existing_user(get_test_group: Generator[groups.Group], get_test_user: Generator[users.User]) -> None: + gr = get_test_group + u = get_test_user + (uid, uname) = (u.id, u.name) + u.name = util.NON_EXISTING_KEY + u.id = util.NON_EXISTING_KEY + with pytest.raises(exceptions.ObjectNotFound): + gr.add_user(u) + (u.name, u.id) = (uid, uname) + + +def test_remove_non_existing_user(get_test_group: Generator[groups.Group], get_test_user: Generator[users.User]) -> None: + util.start_logging() + gr = get_test_group + u = get_test_user + with pytest.raises(exceptions.ObjectNotFound): + gr.remove_user(u) + gr.add_user(u) + u.id = util.NON_EXISTING_KEY + u.login = util.NON_EXISTING_KEY + with pytest.raises(exceptions.ObjectNotFound): + gr.remove_user(u) + + +def test_audit_empty(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + settings = {"audit.groups.empty": True} + assert len(gr.audit(settings)) == 1 + + +def test_to_json(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + json_data = gr.to_json() + assert json_data["name"] == util.TEMP_KEY + assert "description" not in json_data + + assert gr.set_description("A test group") + json_data = gr.to_json() + assert json_data["description"] == "A test group" + + assert not gr.set_description(None) + assert json_data["description"] == "A test group" + + if util.SQ.version() >= (10, 4, 0): + assert "id" in gr.to_json(True) + + +def test_import() -> None: + data = {} + groups.import_config(util.SQ, data) + data = { + "groups": { + "Group1": "This is Group1", + "Group2": "This is Group2", + "Group3": "This is Group3", + } + } + groups.import_config(util.SQ, data) + for g in "Group1", "Group2", "Group3": + assert groups.exists(endpoint=util.SQ, name=g) + o_g = groups.Group.get_object(endpoint=util.SQ, name=g) + assert o_g.description == f"This is {g}" + o_g.delete() + + +def test_convert_yaml() -> None: + data = groups.export(util.SQ, {}) + yaml_list = groups.convert_for_yaml(data) + assert len(yaml_list) == len(data) + assert len(yaml_list[0]) == 2 From 4dfbe71b1125c417db232f9b92e0904368db27b7 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 27 Dec 2024 11:39:41 +0100 Subject: [PATCH 16/22] Improve-users-coverage (#1549) * Fix typo on last Connection date evaluation * Remove bad debug log * Fix cache pop with default * Fix docstring * Add more user tests * Add more group tests * Better params order * Add users.exists() * Formatting * Fix * Fix args of exists() * Revert fix * Fix test_create_or_update * Fix set_name() to return false if name is unchanged * Fix update if at least 1 param changed * Test update login --- sonar/groups.py | 12 +++----- sonar/users.py | 28 ++++++++++-------- test/test_groups.py | 17 +++++++++++ test/test_users.py | 71 +++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 26 deletions(-) diff --git a/sonar/groups.py b/sonar/groups.py index 703966d4f..506a9029c 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -200,7 +200,6 @@ def members(self, use_cache: bool = True) -> list[users.User]: if self.__members is None or not use_cache: if self.endpoint.version() >= (10, 4, 0): data = json.loads(self.get(MEMBERSHIP_API, params={"groupId": self.id}).text)["groupMemberships"] - log.debug("MEMBER DATA = %s", util.json_dump(data)) self.__members = [users.User.get_object_by_id(self.endpoint, d["userId"]) for d in data] else: data = self.endpoint.get_paginated("api/user_groups/users", return_field="users", params={"name": self.name}) @@ -305,9 +304,8 @@ def to_json(self, full_specs: bool = False) -> types.ObjectJsonRepr: def set_description(self, description: str) -> bool: """Set a group description - :param str description: The new group description + :param description: The new group description :return: Whether the new description was successfully set - :rtype: bool """ if description is None or description == self.description: log.debug("No description to update for %s", str(self)) @@ -324,13 +322,12 @@ def set_description(self, description: str) -> bool: def set_name(self, name: str) -> bool: """Set a group name - :param str name: The new group name + :param name: The new group name :return: Whether the new description was successfully set - :rtype: bool """ if name is None or name == self.name: log.debug("No name to update for %s", str(self)) - return True + return False log.debug("Updating %s with name = %s", str(self), name) if self.endpoint.version() >= (10, 4, 0): r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", params={"name": name}) @@ -349,7 +346,6 @@ def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, G :params Platform endpoint: Reference to the SonarQube platform :return: dict of groups with group name as key - :rtype: dict{name: Group} """ return sq.search_objects(endpoint=endpoint, object_class=Group, params=params, api_version=2) @@ -461,7 +457,7 @@ def exists(endpoint: pf.Platform, name: str) -> bool: :param group_name: group name to check :return: whether the group exists """ - return Group.get_object(name=name, endpoint=endpoint) is not None + return Group.get_object(endpoint=endpoint, name=name) is not None def convert_for_yaml(original_json: types.ObjectJsonRepr) -> types.ObjectJsonRepr: diff --git a/sonar/users.py b/sonar/users.py index c58c04851..dd8f0682f 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -200,7 +200,7 @@ def __load(self, data: types.ApiPayload) -> None: self.nb_tokens = data.get("tokenCount", None) #: Nbr of tokens - read-only else: dt1 = util.string_to_date(data.get("sonarQubeLastConnectionDate", None)) - dt2 = util.string_to_date(data.get("sonarQubeLastConnectionDate", None)) + dt2 = util.string_to_date(data.get("sonarLintLastConnectionDate", None)) if not dt1: self.last_login = dt2 elif not dt2: @@ -263,24 +263,19 @@ def tokens(self, **kwargs) -> list[tokens.UserToken]: def update(self, **kwargs) -> User: """Updates a user with name, email, login, SCM accounts, group memberships - :param name: New name of the user - :type name: str, optional - :param email: New email of the user - :type email: str, optional - :param login: New login of the user - :type login: str, optional - :param KeyList groups: List of groups to add membership - :param scm_accounts: List of SCM accounts - :type scm_accounts: list[str], optional + :param str name: Optional, New name of the user + :param str email: Optional, New email of the user + :param str login: Optional, New login of the user + :param list[str] groups: Optional, List of groups to add membership + :param list[str] scmAccounts: Optional, List of SCM accounts :return: self - :rtype: User """ log.debug("Updating %s with %s", str(self), str(kwargs)) params = self.api_params(c.UPDATE) my_data = vars(self) if self.is_local: params.update({k: kwargs[k] for k in ("name", "email") if k in kwargs and kwargs[k] != my_data[k]}) - if len(params) > 1: + if len(params) >= 1: self.post(User.API[c.UPDATE], params=params) if "scmAccounts" in kwargs: self.set_scm_accounts(kwargs["scmAccounts"]) @@ -571,3 +566,12 @@ def import_config(endpoint: pf.Platform, config_data: types.ObjectJsonRepr, key_ def convert_for_yaml(original_json: types.ObjectJsonRepr) -> types.ObjectJsonRepr: """Convert the original JSON defined for JSON export into a JSON format more adapted for YAML export""" return util.dict_to_list(original_json, "login") + + +def exists(endpoint: pf.Platform, login: str) -> bool: + """ + :param endpoint: reference to the SonarQube platform + :param login: user login to check + :return: whether the group exists + """ + return User.get_object(endpoint=endpoint, login=login) is not None diff --git a/test/test_groups.py b/test/test_groups.py index a6cd29eb4..ec974c3f7 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -147,3 +147,20 @@ def test_convert_yaml() -> None: yaml_list = groups.convert_for_yaml(data) assert len(yaml_list) == len(data) assert len(yaml_list[0]) == 2 + + +def test_set_name(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + assert gr.name == util.TEMP_KEY + assert not gr.set_name(gr.name) + assert not gr.set_name(None) + assert gr.name == util.TEMP_KEY + gr.set_name("FOOBAR") + assert gr.name == "FOOBAR" + + +def test_create_or_update(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + gr2 = groups.create_or_update(util.SQ, gr.name, "Some new group description") + assert gr2 is gr + assert gr.description == "Some new group description" diff --git a/test/test_users.py b/test/test_users.py index a21addef0..6f44d0ac1 100644 --- a/test/test_users.py +++ b/test/test_users.py @@ -89,8 +89,8 @@ def test_remove_from_group(get_test_user: Generator[users.User]) -> None: user.remove_from_group("non-existing-group") -def test_set_groups(get_test_user: Generator[users.User]) -> None: - """test_set_groups""" +def test_set_groups_2(get_test_user: Generator[users.User]) -> None: + """test_set_groups_2""" user = get_test_user # TODO(@okorach): Pick groups that exist in SonarQube groups = ["quality-managers", "tech-leads"] @@ -118,10 +118,6 @@ def test_scm_accounts(get_test_user: Generator[users.User]) -> None: assert sorted(user.scm_accounts) == sorted(list(set(scm_1) | set(scm_2))) -def test_double_match(get_test_user: Generator[users.User]) -> None: - """test_scm_accounts""" - - def test_audit_user() -> None: """audit_user""" logging.set_logger(util.TEST_LOGFILE) @@ -169,3 +165,66 @@ def test_more_than_50_users(get_60_users: Generator[list[users.User]]) -> None: new_user_list = users.get_list(util.SQ) assert len(new_user_list) > 60 assert set(new_user_list.keys()) > set(u.name for u in user_list) + + +def test_update(get_test_user: Generator[users.User]) -> None: + # test_update + user = get_test_user + assert user.groups() == ["sonar-users"] + assert user.login == util.TEMP_KEY + assert user.name == f"User name {util.TEMP_KEY}" + + user.update(groups=["sonar-administrators"]) + assert sorted(user.groups()) == ["sonar-administrators", "sonar-users"] + + assert user.scm_accounts is None + + user.update(scmAccounts=["foo@gmail.com", "bar@gmail.com", "foo", "bar"]) + assert sorted(user.scm_accounts) == sorted(["foo@gmail.com", "bar@gmail.com", "foo", "bar"]) + + user.update(login="johndoe") + assert user.login == "johndoe" + + user.update(name="John Doe", email="john@doe.com") + assert user.name == "John Doe" + assert user.email == "john@doe.com" + + user.update(login="jdoe", email="john@doe.com") + assert user.login == "jdoe" + + +def test_set_groups(get_test_user: Generator[users.User]) -> None: + user = get_test_user + user.set_groups(["sonar-administrators", "language-experts"]) + assert sorted(user.groups()) == sorted(["sonar-administrators", "language-experts"]) + + user.set_groups(["language-experts", "security-auditors", "developers"]) + assert sorted(user.groups()) == sorted(["language-experts", "security-auditors", "developers"]) + + +def test_import() -> None: + data = {} + users.import_config(util.SQ, data) + data = { + "users": { + "TEMP": {"local": True, "name": "User name TEMP", "scmAccounts": "temp@acme.com, temp@gmail.com"}, + "TEMP_ADMIN": { + "email": "admin@acme.com", + "groups": "sonar-administrators", + "local": True, + "name": "User name TEMP_ADMIN", + "scmAccounts": "admin-acme, administrator-acme", + }, + } + } + users.import_config(util.SQ, data) + for uname in "TEMP", "TEMP_ADMIN": + assert users.exists(endpoint=util.SQ, login=uname) + o_g = users.User.get_object(endpoint=util.SQ, login=uname) + assert o_g.description == f"User name {uname}" + o_g.delete() + + +def test_deactivate(get_test_user: Generator[users.User]) -> None: + user = get_test_user + assert user.deactivate() From 32d344208e8935eff6386b989dc95a0f4919e2cb Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Thu, 2 Jan 2025 16:17:10 +0100 Subject: [PATCH 17/22] Fix-lts (#1550) * Fix members() for API v1 * Add test_lts if exists * Add test_lts * Add dynamic test for presence of test reports * Add dynamic test for presence of test report * Ignore test_lts * Add credentials per target platform * Ignore temp test files * Add test preparation * Move unit tests in subdir * Move unit tests in subdir * Move integration tests in subdir * Add tests preparation * Don't run sonarcloud tests separately * Run all available temp tests * Use test/ subdir for test exclusions * Update new location of tests * Move conftest in test/unit * Fix conftest.py and utilities.py file name * MOve sample files in test/files * Make test files root configurable * Formatting * Use common constant for test files root * Formatting * Fix prep_test in yaml * Fix pytest output file extension * Remove temp tests after scanning * Quality pass * Quality pass --- .github/workflows/build.yml | 15 ++++++-- .gitignore | 7 +++- conf/prep_tests.sh | 38 +++++++++++++++++++ conf/run_tests.sh | 16 ++++++-- conf/scan.sh | 18 ++++++--- sonar-project.properties | 2 +- sonar/groups.py | 6 +-- test/{ => files}/sif.dce.1.json | 0 test/{ => files}/sif.dce.2.json | 0 test/{ => files}/sif1.json | 0 test/{ => files}/sif2.json | 0 test/{ => files}/sif_broken.json | 0 test/{ => files}/sif_not_readable.json | 0 .../file-with-ambiguous-issues.py | 0 test/{ => integration}/it-docker.sh | 0 test/{ => integration}/it-test-docker.sh | 0 test/{ => integration}/it-tools.sh | 0 test/{ => integration}/it.sh | 0 test/{ => unit}/conftest.py | 0 test/unit/credentials-cloud.py | 25 ++++++++++++ test/unit/credentials-latest.py | 25 ++++++++++++ test/unit/credentials-lts.py | 25 ++++++++++++ test/unit/credentials.py | 25 ++++++++++++ test/{ => unit}/test_apps.py | 0 test/{ => unit}/test_audit.py | 2 +- test/{ => unit}/test_config.py | 0 test/{ => unit}/test_devops.py | 0 test/{ => unit}/test_findings.py | 0 test/{ => unit}/test_findings_sync.py | 0 test/{ => unit}/test_groups.py | 0 test/{ => unit}/test_housekeeper.py | 0 test/{ => unit}/test_issues.py | 0 test/{ => unit}/test_loc.py | 0 test/{ => unit}/test_logging.py | 0 test/{ => unit}/test_measures.py | 0 test/{ => unit}/test_migration.py | 0 test/{ => unit}/test_portfolios.py | 0 test/{ => unit}/test_project_export.py | 0 test/{ => unit}/test_projects.py | 0 test/{ => unit}/test_qp.py | 0 test/{ => unit}/test_rules.py | 0 test/{ => unit}/test_sif.py | 18 ++++----- test/{ => unit}/test_sonarcloud.py | 0 test/{ => unit}/test_sqobject.py | 0 test/{ => unit}/test_tasks.py | 0 test/{ => unit}/test_users.py | 0 test/{ => unit}/utilities.py | 8 +++- 47 files changed, 199 insertions(+), 31 deletions(-) create mode 100755 conf/prep_tests.sh rename test/{ => files}/sif.dce.1.json (100%) rename test/{ => files}/sif.dce.2.json (100%) rename test/{ => files}/sif1.json (100%) rename test/{ => files}/sif2.json (100%) rename test/{ => files}/sif_broken.json (100%) rename test/{ => files}/sif_not_readable.json (100%) rename test/{ => integration}/file-with-ambiguous-issues.py (100%) rename test/{ => integration}/it-docker.sh (100%) rename test/{ => integration}/it-test-docker.sh (100%) rename test/{ => integration}/it-tools.sh (100%) rename test/{ => integration}/it.sh (100%) rename test/{ => unit}/conftest.py (100%) create mode 100644 test/unit/credentials-cloud.py create mode 100644 test/unit/credentials-latest.py create mode 100644 test/unit/credentials-lts.py create mode 100644 test/unit/credentials.py rename test/{ => unit}/test_apps.py (100%) rename test/{ => unit}/test_audit.py (98%) rename test/{ => unit}/test_config.py (100%) rename test/{ => unit}/test_devops.py (100%) rename test/{ => unit}/test_findings.py (100%) rename test/{ => unit}/test_findings_sync.py (100%) rename test/{ => unit}/test_groups.py (100%) rename test/{ => unit}/test_housekeeper.py (100%) rename test/{ => unit}/test_issues.py (100%) rename test/{ => unit}/test_loc.py (100%) rename test/{ => unit}/test_logging.py (100%) rename test/{ => unit}/test_measures.py (100%) rename test/{ => unit}/test_migration.py (100%) rename test/{ => unit}/test_portfolios.py (100%) rename test/{ => unit}/test_project_export.py (100%) rename test/{ => unit}/test_projects.py (100%) rename test/{ => unit}/test_qp.py (100%) rename test/{ => unit}/test_rules.py (100%) rename test/{ => unit}/test_sif.py (86%) rename test/{ => unit}/test_sonarcloud.py (100%) rename test/{ => unit}/test_sqobject.py (100%) rename test/{ => unit}/test_tasks.py (100%) rename test/{ => unit}/test_users.py (100%) rename test/{ => unit}/utilities.py (95%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4fd0db35..3b6c9f23f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,13 +39,20 @@ jobs: # - name: Test with pytest # run: | # pytest + + - name: Prep tests + working-directory: . + run: | + chmod +x conf/prep_tests.sh + conf/prep_tests.sh + # - name: Run tests # working-directory: . # run: | - # chmod +x ./run_tests.sh - # ./run_tests.sh - # echo "--------- UT report ---------"; cat build/ut.xml - # echo "---------Coverage report ---------"; cat build/coverage.xml + # chmod +x conf/run_tests.sh + # conf/run_tests.sh + # echo "--------- UT report ---------"; cat build/xunit*.xml + # echo "---------Coverage report ---------"; cat build/coverage*.xml - name: Run linters working-directory: . run: | diff --git a/.gitignore b/.gitignore index 8e64d121d..1371b48f8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,10 @@ tmp/ .DS_Store !test/config.json -!test/sif*.json +!test/integration/sif*.json !.vscode/*.json !test/files/* -.sonar/ \ No newline at end of file +.sonar/ +test/lts/ +test/latest/ +test/cloud/ diff --git a/conf/prep_tests.sh b/conf/prep_tests.sh new file mode 100755 index 000000000..a5a7e4265 --- /dev/null +++ b/conf/prep_tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +ROOTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" + +cd "$ROOTDIR/test/unit" || exit 1 + +for target in lts latest +do + rm -rf "$ROOTDIR/test/$target" + mkdir -p "$ROOTDIR/test/$target" 2>/dev/null + for f in *.py + do + b=$(basename "$f" .py) + cp "$f" "$ROOTDIR/test/$target/${b}_${target}.py" + done + cp "credentials-$target.py" "$ROOTDIR/test/$target/credentials.py" + mv "$ROOTDIR/test/$target/conftest_${target}.py" "$ROOTDIR/test/$target/conftest.py" + mv "$ROOTDIR/test/$target/utilities_${target}.py" "$ROOTDIR/test/$target/utilities.py" +done diff --git a/conf/run_tests.sh b/conf/run_tests.sh index 416e14c75..27a87b6ed 100755 --- a/conf/run_tests.sh +++ b/conf/run_tests.sh @@ -23,12 +23,20 @@ ME="$( basename "${BASH_SOURCE[0]}" )" ROOTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" CONFDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" buildDir="$ROOTDIR/build" -coverageReport="$buildDir/coverage.xml" -utReport="$buildDir/xunit-results.xml" + [ ! -d $buildDir ] && mkdir $buildDir echo "Running tests" + +"$CONFDIR/prep_tests.sh" + export SONAR_HOST_URL=${1:-${SONAR_HOST_URL}} -coverage run --branch --source=$ROOTDIR -m pytest $ROOTDIR/test/ --junit-xml="$utReport" -coverage xml -o $coverageReport +for target in latest lts cloud +do + if [ -d "$ROOTDIR/test/$target/" ]; then + coverage run --branch --source="$ROOTDIR" -m pytest "$ROOTDIR/test/$target/" --junit-xml="$buildDir/xunit-results-$target.xml" + coverage xml -o "$buildDir/coverage-$target.xml" + fi +done + diff --git a/conf/scan.sh b/conf/scan.sh index 99c7ff598..e22f7c30b 100755 --- a/conf/scan.sh +++ b/conf/scan.sh @@ -46,7 +46,7 @@ do shift done -buildDir="$ROOTDIR/build" +buildDir="build" pylintReport="$buildDir/pylint-report.out" banditReport="$buildDir/bandit-report.json" flake8Report="$buildDir/flake8-report.out" @@ -77,15 +77,23 @@ cmd="sonar-scanner -Dsonar.projectVersion=$version \ -Dsonar.token=$SONAR_TOKEN \ "${scanOpts[*]}"" -if [ -f "$coverageReport" ]; then - cmd="$cmd -Dsonar.python.coverage.reportPaths=$coverageReport" +if ls $buildDir/coverage*.xml >/dev/null 2>&1; then + cmd="$cmd -Dsonar.python.coverage.reportPaths=$buildDir/coverage*.xml" +else + echo "===> NO COVERAGE REPORT" fi -if [ -f "$utReport" ]; then - cmd="$cmd -Dsonar.python.xunit.reportPath=$utReport" +if ls $buildDir/xunit-results*.xml >/dev/null 2>&1; then + cmd="$cmd -Dsonar.python.xunit.reportPath=$buildDir/xunit-results*.xml" else + echo "===> NO UNIT TESTS REPORT" cmd="$cmd -Dsonar.python.xunit.reportPath=" fi echo "Running: $cmd" $cmd + +for target in lts latest +do + rm -rf "$ROOTDIR/test/$target" +done \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index da2a81467..d6ef40e91 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -17,4 +17,4 @@ sonar.sarifReportPaths=build/results_sarif.sarif sonar.exclusions=doc/api/**/*, build/**/*, test/**/* sonar.coverage.exclusions=setup*.py, test/**/*, conf/*2sonar.py, cli/cust_measures.py, sonar/custom_measures.py, cli/support.py, cli/projects_export.py, cli/projects_import.py sonar.cpd.exclusions=setup*.py -sonar.tests=test +sonar.tests=test/unit diff --git a/sonar/groups.py b/sonar/groups.py index 506a9029c..2521727c9 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -199,11 +199,11 @@ def members(self, use_cache: bool = True) -> list[users.User]: """Returns the group members""" if self.__members is None or not use_cache: if self.endpoint.version() >= (10, 4, 0): - data = json.loads(self.get(MEMBERSHIP_API, params={"groupId": self.id}).text)["groupMemberships"] - self.__members = [users.User.get_object_by_id(self.endpoint, d["userId"]) for d in data] + data = json.loads(self.get(MEMBERSHIP_API, params={"groupId": self.id}).text) + self.__members = [users.User.get_object_by_id(self.endpoint, d["userId"]) for d in data["groupMemberships"]] else: data = self.endpoint.get_paginated("api/user_groups/users", return_field="users", params={"name": self.name}) - self.__members = [users.User.get_object(self.endpoint, d["login"]) for d in data] + self.__members = [users.User.get_object(self.endpoint, d["login"]) for d in data["users"]] return self.__members def size(self) -> int: diff --git a/test/sif.dce.1.json b/test/files/sif.dce.1.json similarity index 100% rename from test/sif.dce.1.json rename to test/files/sif.dce.1.json diff --git a/test/sif.dce.2.json b/test/files/sif.dce.2.json similarity index 100% rename from test/sif.dce.2.json rename to test/files/sif.dce.2.json diff --git a/test/sif1.json b/test/files/sif1.json similarity index 100% rename from test/sif1.json rename to test/files/sif1.json diff --git a/test/sif2.json b/test/files/sif2.json similarity index 100% rename from test/sif2.json rename to test/files/sif2.json diff --git a/test/sif_broken.json b/test/files/sif_broken.json similarity index 100% rename from test/sif_broken.json rename to test/files/sif_broken.json diff --git a/test/sif_not_readable.json b/test/files/sif_not_readable.json similarity index 100% rename from test/sif_not_readable.json rename to test/files/sif_not_readable.json diff --git a/test/file-with-ambiguous-issues.py b/test/integration/file-with-ambiguous-issues.py similarity index 100% rename from test/file-with-ambiguous-issues.py rename to test/integration/file-with-ambiguous-issues.py diff --git a/test/it-docker.sh b/test/integration/it-docker.sh similarity index 100% rename from test/it-docker.sh rename to test/integration/it-docker.sh diff --git a/test/it-test-docker.sh b/test/integration/it-test-docker.sh similarity index 100% rename from test/it-test-docker.sh rename to test/integration/it-test-docker.sh diff --git a/test/it-tools.sh b/test/integration/it-tools.sh similarity index 100% rename from test/it-tools.sh rename to test/integration/it-tools.sh diff --git a/test/it.sh b/test/integration/it.sh similarity index 100% rename from test/it.sh rename to test/integration/it.sh diff --git a/test/conftest.py b/test/unit/conftest.py similarity index 100% rename from test/conftest.py rename to test/unit/conftest.py diff --git a/test/unit/credentials-cloud.py b/test/unit/credentials-cloud.py new file mode 100644 index 000000000..c623e3c83 --- /dev/null +++ b/test/unit/credentials-cloud.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "https://sonarcloud.io" +TARGET_TOKEN = getenv("SONAR_TOKEN_SONARCLOUD") diff --git a/test/unit/credentials-latest.py b/test/unit/credentials-latest.py new file mode 100644 index 000000000..753f3b9c3 --- /dev/null +++ b/test/unit/credentials-latest.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "http://localhost:10000" +TARGET_TOKEN = getenv("SONAR_TOKEN_LATEST_ADMIN_USER") diff --git a/test/unit/credentials-lts.py b/test/unit/credentials-lts.py new file mode 100644 index 000000000..4407fe781 --- /dev/null +++ b/test/unit/credentials-lts.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "http://localhost:9000" +TARGET_TOKEN = getenv("SONAR_TOKEN_LTS_ADMIN_USER") diff --git a/test/unit/credentials.py b/test/unit/credentials.py new file mode 100644 index 000000000..753f3b9c3 --- /dev/null +++ b/test/unit/credentials.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "http://localhost:10000" +TARGET_TOKEN = getenv("SONAR_TOKEN_LATEST_ADMIN_USER") diff --git a/test/test_apps.py b/test/unit/test_apps.py similarity index 100% rename from test/test_apps.py rename to test/unit/test_apps.py diff --git a/test/test_audit.py b/test/unit/test_audit.py similarity index 98% rename from test/test_audit.py rename to test/unit/test_audit.py index 0edb2f2bb..554f3f1f3 100644 --- a/test/test_audit.py +++ b/test/unit/test_audit.py @@ -109,7 +109,7 @@ def test_sif_non_existing(get_csv_file: Generator[str]) -> None: def test_sif_not_readable(get_json_file: Generator[str]) -> None: """test_sif_not_readable""" - unreadable_file = "test/sif_not_readable.json" + unreadable_file = f"{util.FILES_ROOT}/sif_not_readable.json" NO_PERMS = ~stat.S_IRUSR & ~stat.S_IWUSR current_permissions = stat.S_IMODE(os.lstat(unreadable_file).st_mode) os.chmod(unreadable_file, current_permissions & NO_PERMS) diff --git a/test/test_config.py b/test/unit/test_config.py similarity index 100% rename from test/test_config.py rename to test/unit/test_config.py diff --git a/test/test_devops.py b/test/unit/test_devops.py similarity index 100% rename from test/test_devops.py rename to test/unit/test_devops.py diff --git a/test/test_findings.py b/test/unit/test_findings.py similarity index 100% rename from test/test_findings.py rename to test/unit/test_findings.py diff --git a/test/test_findings_sync.py b/test/unit/test_findings_sync.py similarity index 100% rename from test/test_findings_sync.py rename to test/unit/test_findings_sync.py diff --git a/test/test_groups.py b/test/unit/test_groups.py similarity index 100% rename from test/test_groups.py rename to test/unit/test_groups.py diff --git a/test/test_housekeeper.py b/test/unit/test_housekeeper.py similarity index 100% rename from test/test_housekeeper.py rename to test/unit/test_housekeeper.py diff --git a/test/test_issues.py b/test/unit/test_issues.py similarity index 100% rename from test/test_issues.py rename to test/unit/test_issues.py diff --git a/test/test_loc.py b/test/unit/test_loc.py similarity index 100% rename from test/test_loc.py rename to test/unit/test_loc.py diff --git a/test/test_logging.py b/test/unit/test_logging.py similarity index 100% rename from test/test_logging.py rename to test/unit/test_logging.py diff --git a/test/test_measures.py b/test/unit/test_measures.py similarity index 100% rename from test/test_measures.py rename to test/unit/test_measures.py diff --git a/test/test_migration.py b/test/unit/test_migration.py similarity index 100% rename from test/test_migration.py rename to test/unit/test_migration.py diff --git a/test/test_portfolios.py b/test/unit/test_portfolios.py similarity index 100% rename from test/test_portfolios.py rename to test/unit/test_portfolios.py diff --git a/test/test_project_export.py b/test/unit/test_project_export.py similarity index 100% rename from test/test_project_export.py rename to test/unit/test_project_export.py diff --git a/test/test_projects.py b/test/unit/test_projects.py similarity index 100% rename from test/test_projects.py rename to test/unit/test_projects.py diff --git a/test/test_qp.py b/test/unit/test_qp.py similarity index 100% rename from test/test_qp.py rename to test/unit/test_qp.py diff --git a/test/test_rules.py b/test/unit/test_rules.py similarity index 100% rename from test/test_rules.py rename to test/unit/test_rules.py diff --git a/test/test_sif.py b/test/unit/test_sif.py similarity index 86% rename from test/test_sif.py rename to test/unit/test_sif.py index f3f3a0656..dad1cfb80 100644 --- a/test/test_sif.py +++ b/test/unit/test_sif.py @@ -43,7 +43,7 @@ def test_audit_sif() -> None: """test_audit_sif""" util.clean(util.CSV_FILE) with pytest.raises(SystemExit) as e: - with patch.object(sys, "argv", [CMD, "--sif", "test/sif1.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): + with patch.object(sys, "argv", [CMD, "--sif", f"{util.FILES_ROOT}/sif1.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): audit.main() assert int(str(e.value)) == errcodes.OK assert util.file_not_empty(util.CSV_FILE) @@ -54,7 +54,7 @@ def test_audit_sif_dce1() -> None: """test_audit_sif_dce1""" util.clean(util.CSV_FILE) with pytest.raises(SystemExit) as e: - with patch.object(sys, "argv", [CMD, "--sif", "test/sif.dce.1.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): + with patch.object(sys, "argv", [CMD, "--sif", f"{util.FILES_ROOT}/sif.dce.1.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): audit.main() assert int(str(e.value)) == errcodes.OK assert util.file_not_empty(util.CSV_FILE) @@ -65,7 +65,7 @@ def test_audit_sif_dce2() -> None: """test_audit_sif_dce2""" util.clean(util.CSV_FILE) with pytest.raises(SystemExit) as e: - with patch.object(sys, "argv", [CMD, "--sif", "test/sif.dce.2.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): + with patch.object(sys, "argv", [CMD, "--sif", f"{util.FILES_ROOT}/sif.dce.2.json", f"--{opt.REPORT_FILE}", util.CSV_FILE]): audit.main() assert int(str(e.value)) == errcodes.OK assert util.file_not_empty(util.CSV_FILE) @@ -76,7 +76,7 @@ def test_sif_1() -> None: """test_sif_1""" util.clean(util.CSV_FILE) with pytest.raises(SystemExit) as e: - with patch.object(sys, "argv", CSV_OPTS + ["--sif", "test/sif1.json"]): + with patch.object(sys, "argv", CSV_OPTS + ["--sif", f"{util.FILES_ROOT}/sif1.json"]): audit.main() assert int(str(e.value)) == errcodes.OK assert util.file_not_empty(util.CSV_FILE) @@ -87,7 +87,7 @@ def test_sif_2() -> None: """test_sif_2""" util.clean(util.JSON_FILE) with pytest.raises(SystemExit) as e: - with patch.object(sys, "argv", JSON_OPTS + ["--sif", "test/sif2.json"]): + with patch.object(sys, "argv", JSON_OPTS + ["--sif", f"{util.FILES_ROOT}/sif2.json"]): audit.main() assert int(str(e.value)) == errcodes.OK assert util.file_not_empty(util.JSON_FILE) @@ -96,7 +96,7 @@ def test_sif_2() -> None: def test_audit_sif_ut() -> None: """test_audit_sif_ut""" - with open("test/sif1.json", "r", encoding="utf-8") as f: + with open(f"{util.FILES_ROOT}/sif1.json", "r", encoding="utf-8") as f: json_sif = json.loads(f.read()) sysinfo = sif.Sif(json_sif) assert sysinfo.edition() == "enterprise" @@ -118,7 +118,7 @@ def test_audit_sif_ut() -> None: def test_modified_sif() -> None: """test_modified_sif""" - with open("test/sif1.json", "r", encoding="utf-8") as f: + with open("test/files/sif1.json", "r", encoding="utf-8") as f: json_sif = json.loads(f.read()) json_sif["System"].pop("Edition") @@ -135,7 +135,7 @@ def test_modified_sif() -> None: def test_json_not_sif() -> None: """Tests that the right exception is raised if JSON file is not a SIF""" with pytest.raises(sif.NotSystemInfo) as e: - with open("test/config.json", "r", encoding="utf-8") as f: + with open("test/files/config.json", "r", encoding="utf-8") as f: json_sif = json.loads(f.read()) _ = sif.Sif(json_sif) assert e.type == sif.NotSystemInfo @@ -143,7 +143,7 @@ def test_json_not_sif() -> None: def test_dce_sif_ut() -> None: """test_audit_sif_ut""" - with open("test/sif.dce.1.json", "r", encoding="utf-8") as f: + with open("test/files/sif.dce.1.json", "r", encoding="utf-8") as f: json_sif = json.loads(f.read()) sysinfo = sif.Sif(json_sif) diff --git a/test/test_sonarcloud.py b/test/unit/test_sonarcloud.py similarity index 100% rename from test/test_sonarcloud.py rename to test/unit/test_sonarcloud.py diff --git a/test/test_sqobject.py b/test/unit/test_sqobject.py similarity index 100% rename from test/test_sqobject.py rename to test/unit/test_sqobject.py diff --git a/test/test_tasks.py b/test/unit/test_tasks.py similarity index 100% rename from test/test_tasks.py rename to test/unit/test_tasks.py diff --git a/test/test_users.py b/test/unit/test_users.py similarity index 100% rename from test/test_users.py rename to test/unit/test_users.py diff --git a/test/utilities.py b/test/unit/utilities.py similarity index 95% rename from test/utilities.py rename to test/unit/utilities.py index 7fbf73300..fd51f499d 100644 --- a/test/utilities.py +++ b/test/unit/utilities.py @@ -30,18 +30,22 @@ from unittest.mock import patch import pytest +import credentials as creds from sonar import errcodes, logging from sonar import platform import cli.options as opt TEST_LOGFILE = "pytest.log" LOGGER_COUNT = 0 +FILES_ROOT = "test/files/" LATEST = "http://localhost:10000" LATEST_TEST = "http://localhost:10020" LTA = "http://localhost:9000" LATEST_CE = "http://localhost:8000" +TARGET_PLATFORM = LATEST + CSV_FILE = f"temp.{os.getpid()}.csv" JSON_FILE = f"temp.{os.getpid()}.json" YAML_FILE = f"temp.{os.getpid()}.yaml" @@ -61,7 +65,7 @@ TEMP_KEY_3 = "TEMP3" TEMP_NAME = "Temp Name" -STD_OPTS = [f"-{opt.URL_SHORT}", os.getenv("SONAR_HOST_URL"), f"-{opt.TOKEN_SHORT}", os.getenv("SONAR_TOKEN_ADMIN_USER")] +STD_OPTS = [f"-{opt.URL_SHORT}", creds.TARGET_PLATFORM, f"-{opt.TOKEN_SHORT}", creds.TARGET_TOKEN] SQS_OPTS = " ".join(STD_OPTS) TEST_OPTS = [f"-{opt.URL_SHORT}", LATEST_TEST, f"-{opt.TOKEN_SHORT}", os.getenv("SONAR_TOKEN_ADMIN_USER")] @@ -71,7 +75,7 @@ SC_OPTS = f'--{opt.URL} https://sonarcloud.io --{opt.TOKEN} {os.getenv("SONAR_TOKEN_SONARCLOUD")} --{opt.ORG} okorach' -SQ = platform.Platform(url=os.getenv("SONAR_HOST_URL"), token=os.getenv("SONAR_TOKEN_ADMIN_USER")) +SQ = platform.Platform(url=creds.TARGET_PLATFORM, token=creds.TARGET_TOKEN) SC = platform.Platform(url="https://sonarcloud.io", token=os.getenv("SONAR_TOKEN_SONARCLOUD")) TEST_SQ = platform.Platform(url=LATEST_TEST, token=os.getenv("SONAR_TOKEN_ADMIN_USER")) From f8169d420974575d7229f81a067cd6ed0f6a744a Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 3 Jan 2025 18:22:44 +0100 Subject: [PATCH 18/22] More tests (#1551) * Use is_default() function * MOre group tests * Safety on app deletion * Use check_supported() * More app tests * Formatting * Fix API v1 for 9.9 * Fix test * Fix test for 9.9 * Fix test * Fix typo * Fix logs * Fix bugs on 9.9 * Fix tests * Adjust tests for 9.9 * Adjust path due to reorg of directories --- sonar/applications.py | 42 ++++++++++++++------------------ sonar/groups.py | 8 +++---- sonar/qualityprofiles.py | 20 +++++++--------- sonar/users.py | 19 +++++++++++---- test/integration/it-tools.sh | 2 +- test/unit/conftest.py | 4 ++++ test/unit/test_apps.py | 46 +++++++++++++++++++++++++++++++++++- test/unit/test_groups.py | 35 +++++++++++++++++++++++---- test/unit/test_users.py | 19 ++++++++------- 9 files changed, 136 insertions(+), 59 deletions(-) diff --git a/sonar/applications.py b/sonar/applications.py index 4e11c1f25..4c930cffa 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -460,9 +460,8 @@ def _project_list(data: types.ObjectJsonRepr) -> types.KeyList: def count(endpoint: pf.Platform) -> int: """returns count of applications - :param pf.Platform endpoint: Reference to the SonarQube platform + :param endpoint: Reference to the SonarQube platform :return: Count of applications - :rtype: int """ check_supported(endpoint) return util.nbr_total_elements(json.loads(endpoint.get(Application.API[c.LIST], params={"ps": 1, "filter": "qualifier = APP"}).text)) @@ -479,11 +478,10 @@ def check_supported(endpoint: pf.Platform) -> None: def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, Application]: """Searches applications - :param Platform endpoint: Reference to the SonarQube platform + :param endpoint: Reference to the SonarQube platform :param params: Search filters (see api/components/search parameters) :raises UnsupportedOperation: If on a community edition :return: dict of applications - :rtype: dict {: Application, ...} """ check_supported(endpoint) new_params = {"filter": "qualifier = APP"} @@ -495,12 +493,12 @@ def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, A def get_list(endpoint: pf.Platform, key_list: types.KeyList = None, use_cache: bool = True) -> dict[str, Application]: """ :return: List of Applications (all of them if key_list is None or empty) - :param Platform endpoint: Reference to the Sonar platform - :param KeyList key_list: List of app keys to get, if None or empty all applications are returned + :param endpoint: Reference to the Sonar platform + :param key_list: List of app keys to get, if None or empty all applications are returned :param use_cache: Whether to use local cache or query SonarQube, default True (use cache) - :type use_cache: bool :rtype: dict{: } """ + check_supported(endpoint) with _CLASS_LOCK: if key_list is None or len(key_list) == 0 or not use_cache: log.info("Listing applications") @@ -521,17 +519,14 @@ def exists(endpoint: pf.Platform, key: str) -> bool: def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> types.ObjectJsonRepr: """Exports applications as JSON - :param Platform endpoint: Reference to the Sonar platform - :param ConfigSetting export_settings: Options to use for export - :param KeyList key_list: list of Application keys to export, defaults to all if None + :param endpoint: Reference to the Sonar platform + :param export_settings: Options to use for export + :param key_list: list of Application keys to export, defaults to all if None :return: Dict of applications settings - :rtype: ObjectJsonRepr """ + check_supported(endpoint) write_q = kwargs.get("write_q", None) key_list = kwargs.get("key_list", None) - if endpoint.is_sonarcloud(): - # log.info("Applications do not exist in SonarCloud, export skipped") - raise exceptions.UnsupportedOperation("Applications do not exist in SonarCloud, export skipped") apps_settings = {} for k, app in get_list(endpoint, key_list).items(): @@ -549,11 +544,10 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) -> list[problem.Problem]: """Audits applications and return list of problems found - :param Platform endpoint: Reference to the Sonar platform - :param dict audit_settings: dict of audit config settings - :param KeyList key_list: list of Application keys to audit, defaults to all if None + :param endpoint: Reference to the Sonar platform + :param audit_settings: dict of audit config settings + :param key_list: list of Application keys to audit, defaults to all if None :return: List of problems found - :rtype: list [Problem] """ if endpoint.edition() == "community": return [] @@ -570,17 +564,17 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) def import_config(endpoint: pf.Platform, config_data: types.ObjectJsonRepr, key_list: types.KeyList = None) -> bool: """Imports a list of application configuration in a SonarQube platform - :param Platform endpoint: Reference to the SonarQube platform - :param dict config_data: JSON representation of applications configuration - :param KeyList key_list: list of Application keys to import, defaults to all if None + :param endpoint: Reference to the SonarQube platform + :param config_data: JSON representation of applications configuration + :param key_list: list of Application keys to import, defaults to all if None :return: Whether import succeeded - :rtype: bool """ if "applications" not in config_data: log.info("No applications to import") return True - if endpoint.edition() == "community": - log.warning("Can't import applications in a community edition") + ed = endpoint.edition() + if ed not in ("developer", "enterprise", "datacenter"): + log.warning("Can't import applications in %s edition", ed) return False log.info("Importing applications") search(endpoint=endpoint) diff --git a/sonar/groups.py b/sonar/groups.py index 2521727c9..6d3d959ce 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -135,7 +135,7 @@ def load(cls, endpoint: pf.Platform, data: types.ApiPayload) -> Group: @classmethod def api_for(cls, op: str, endpoint: object) -> Optional[str]: - """Returns the API for a given operation depedning on the SonarQube version""" + """Returns the API for a given operation depending on the SonarQube version""" if endpoint.is_sonarcloud() or endpoint.version() < (10, 4, 0): api_to_use = Group.API_V1 else: @@ -297,7 +297,7 @@ def to_json(self, full_specs: bool = False) -> types.ObjectJsonRepr: else: json_data = {"name": self.name} json_data["description"] = self.description if self.description and self.description != "" else None - if self.__is_default: + if self.is_default(): json_data["default"] = True return util.remove_nones(json_data) @@ -332,7 +332,7 @@ def set_name(self, name: str) -> bool: if self.endpoint.version() >= (10, 4, 0): r = self.patch(f"{Group.API[c.UPDATE]}/{self.id}", params={"name": name}) else: - r = self.post(Group.API[c.UPDATE], params={"currentName": self.key, "name": name}) + r = self.post(Group.API_V1[c.UPDATE], params={"currentName": self.key, "name": name}) if r.ok: Group.CACHE.pop(self) self.name = name @@ -405,7 +405,7 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) def get_object_from_id(endpoint: pf.Platform, id: str) -> Group: """Searches a Group object from its id - SonarQube 10.4+""" if endpoint.version() < (10, 4, 0): - raise exceptions.UnsupportedOperation + raise exceptions.UnsupportedOperation("Operation unsupported before SonarQube 10.4") if len(Group.CACHE) == 0: get_list(endpoint) for o in Group.CACHE.values(): diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index 20600fa3d..e53631e0f 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -106,7 +106,7 @@ def read(cls, endpoint: pf.Platform, name: str, language: str) -> Optional[Quali :rtype: QualityProfile or None if not found """ if not languages.exists(endpoint=endpoint, language=language): - log.error("Language '%s' does not exist, quality profile creation aborted") + log.error("Language '%s' does not exist, quality profile creation aborted", language) return None log.debug("Reading quality profile '%s' of language '%s'", name, language) o = QualityProfile.CACHE.get(name, language, endpoint.url) @@ -142,12 +142,11 @@ def create(cls, endpoint: pf.Platform, name: str, language: str) -> Optional[Qua def clone(cls, endpoint: pf.Platform, name: str, language: str, original_qp_name: str) -> Optional[QualityProfile]: """Creates a new quality profile in SonarQube with rules copied from original_key - :param Platform endpoint: Reference to the SonarQube platform - :param str name: Quality profile name - :param str language: Quality profile language - :param str original_qp_name: Original quality profile name + :param endpoint: Reference to the SonarQube platform + :param name: Quality profile name + :param language: Quality profile language + :param original_qp_name: Original quality profile name :return: The cloned quality profile object - :rtype: QualityProfile """ log.info("Cloning quality profile name '%s' into quality profile name '%s'", original_qp_name, name) l = [qp for qp in get_list(endpoint, use_cache=False).values() if qp.name == original_qp_name and qp.language == language] @@ -724,12 +723,11 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg def get_object(endpoint: pf.Platform, name: str, language: str) -> Optional[QualityProfile]: """Returns a quality profile Object from its name and language - :param Platform endpoint: Reference to the SonarQube platform - :param str name: Quality profile name - :param str language: Quality profile language + :param endpoint: Reference to the SonarQube platform + :param name: Quality profile name + :param language: Quality profile language :return: The quality profile object, of None if not found - :rtype: QualityProfile or None """ get_list(endpoint) o = QualityProfile.CACHE.get(name, language, endpoint.url) @@ -757,7 +755,7 @@ def __import_thread(queue: Queue) -> None: log.info("Won't import built-in quality profile '%s'", name) queue.task_done() continue - log.info("Qualiy profile '%s' of language '%s' does not exist, creating it", name, lang) + log.info("Quality profile '%s' of language '%s' does not exist, creating it", name, lang) try: # Statistically a new QP is close to Sonar way so better start with the Sonar way ruleset and # add/remove a few rules, than adding all rules from 0 diff --git a/sonar/users.py b/sonar/users.py index dd8f0682f..42b269f20 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -224,10 +224,14 @@ def groups(self, data: types.ApiPayload = None, **kwargs) -> types.KeyList: if data is None: data = self.sq_json self._groups = data.get("groups", []) + if "sonar-users" not in self._groups: + self._groups.append("sonar-users") + log.debug("Updated %s groups = %s", str(self), str(self._groups)) else: data = json.loads(self.get(User.API["GROUP_MEMBERSHIPS"], {"userId": self.id, "pageSize": 500}).text)["groupMemberships"] log.debug("Groups = %s", str(data)) self._groups = [groups.get_object_from_id(self.endpoint, g["groupId"]).name for g in data] + self._groups = sorted(self._groups) return self._groups def refresh(self) -> User: @@ -276,14 +280,20 @@ def update(self, **kwargs) -> User: if self.is_local: params.update({k: kwargs[k] for k in ("name", "email") if k in kwargs and kwargs[k] != my_data[k]}) if len(params) >= 1: - self.post(User.API[c.UPDATE], params=params) + self.post(User.api_for(c.UPDATE, self.endpoint), params=params) + if "name" in params: + self.name = kwargs["name"] + if "email" in params: + self.email = kwargs["email"] if "scmAccounts" in kwargs: self.set_scm_accounts(kwargs["scmAccounts"]) if "login" in kwargs: new_login = kwargs["login"] o = User.CACHE.get(new_login, self.endpoint.url) if not o: - self.post(User.API["UPDATE_LOGIN"], params={**self.api_params(User.API["UPDATE_LOGIN"]), "newLogin": new_login}) + self.post( + User.api_for("UPDATE_LOGIN", self.endpoint), params={**self.api_params(User.API["UPDATE_LOGIN"]), "newLogin": new_login} + ) User.CACHE.pop(self) self.login = new_login User.CACHE.put(self) @@ -305,6 +315,7 @@ def add_to_group(self, group_name: str) -> bool: ok = group.add_user(self) if ok: self._groups.append(group_name) + self._groups = sorted(self._groups) return ok def remove_from_group(self, group_name: str) -> bool: @@ -372,9 +383,7 @@ def set_groups(self, group_list: list[str]) -> bool: for g in list(set(self.groups()) - set(group_list)): if g != "sonar-users": ok = ok and self.remove_from_group(g) - if ok: - self._groups = group_list - else: + if not ok: self.refresh() return ok diff --git a/test/integration/it-tools.sh b/test/integration/it-tools.sh index 72e363d52..8aa37b1de 100644 --- a/test/integration/it-tools.sh +++ b/test/integration/it-tools.sh @@ -21,7 +21,7 @@ #set -euo pipefail -REPO_ROOT="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; cd .. ; pwd -P )" +REPO_ROOT="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; cd ../.. ; pwd -P )" TMP="$REPO_ROOT/tmp" IT_LOG_FILE="$TMP/it.log" mkdir -p "$TMP" diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 8e7bba35c..c57e4bed4 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -119,6 +119,9 @@ def get_test_qp() -> Generator[qualityprofiles.QualityProfile]: util.start_logging() try: o = qualityprofiles.get_object(endpoint=util.SQ, name=util.TEMP_KEY, language="py") + if o.is_default: + sw = qualityprofiles.get_object(endpoint=util.SQ, name="Sonar way", language="py") + sw.set_as_default() except exceptions.ObjectNotFound: o = qualityprofiles.QualityProfile.create(endpoint=util.SQ, name=util.TEMP_KEY, language="py") yield o @@ -215,6 +218,7 @@ def get_test_application() -> Generator[applications.Application]: o = applications.Application.create(endpoint=util.SQ, key=util.TEMP_KEY, name=util.TEMP_KEY) yield o try: + o.key = util.TEMP_KEY o.delete() except exceptions.ObjectNotFound: pass diff --git a/test/unit/test_apps.py b/test/unit/test_apps.py index 96ab90f2a..ca35c2d7e 100644 --- a/test/unit/test_apps.py +++ b/test/unit/test_apps.py @@ -199,7 +199,7 @@ def test_search_by_name() -> None: assert app == first_app -def test_set_tags(get_test_app: callable) -> None: +def test_set_tags(get_test_app: Generator[applications.Application]) -> None: """test_set_tags""" o = get_test_app @@ -212,6 +212,43 @@ def test_set_tags(get_test_app: callable) -> None: assert not o.set_tags(None) +def test_not_found(get_test_app: Generator[applications.Application]) -> None: + """test_not_found""" + if util.SQ.edition() != "community": + o = get_test_app + o.key = "mess-me-up" + with pytest.raises(exceptions.ObjectNotFound): + o.refresh() + + +def test_already_exists(get_test_app: Generator[applications.Application]) -> None: + if util.SQ.edition() != "community": + app = get_test_app + with pytest.raises(exceptions.ObjectAlreadyExists): + _ = applications.Application.create(endpoint=util.SQ, key=app.key, name="Foo Bar") + + +def test_branch_exists(get_test_app: Generator[applications.Application]) -> None: + if util.SQ.edition() != "community": + app = get_test_app + assert app.branch_exists("main") + assert not app.branch_exists("non-existing") + + +def test_branch_is_main(get_test_app: Generator[applications.Application]) -> None: + if util.SQ.edition() != "community": + app = get_test_app + assert app.branch_is_main("main") + with pytest.raises(exceptions.ObjectNotFound): + app.branch_is_main("non-existing") + + +def test_get_issues(get_test_app: Generator[applications.Application]) -> None: + if util.SQ.edition() != "community": + app = get_test_app + assert len(app.get_issues()) == 0 + + def test_audit_disabled() -> None: """test_audit_disabled""" assert len(applications.audit(util.SQ, {"audit.applications": False})) == 0 @@ -240,3 +277,10 @@ def test_app_branches(get_test_application: Generator[applications.Application]) br = app.branches() assert set(br.keys()) >= {"Main Branch", "Master", "MiBranch"} assert app.main_branch().name == "Main Branch" + + +def test_convert_for_yaml() -> None: + if util.SQ.edition() != "community": + data = applications.export(util.SQ, {}) + yaml_list = applications.convert_for_yaml(data) + assert len(yaml_list) == len(data) diff --git a/test/unit/test_groups.py b/test/unit/test_groups.py index ec974c3f7..d48880d95 100644 --- a/test/unit/test_groups.py +++ b/test/unit/test_groups.py @@ -28,6 +28,7 @@ import utilities as util from sonar import exceptions from sonar import groups, users +from sonar.util import constants as c GROUP1 = "sonar-users" GROUP2 = "sonar-administrators" @@ -43,6 +44,9 @@ def test_get_object() -> None: gr2 = groups.Group.get_object(endpoint=util.SQ, name=GROUP2) assert gr is gr2 + gr3 = groups.Group.read(endpoint=util.SQ, name=GROUP2) + assert gr3 is gr + with pytest.raises(exceptions.ObjectNotFound): groups.Group.get_object(endpoint=util.SQ, name=util.NON_EXISTING_KEY) @@ -80,20 +84,17 @@ def test_url() -> None: def test_add_non_existing_user(get_test_group: Generator[groups.Group], get_test_user: Generator[users.User]) -> None: gr = get_test_group u = get_test_user - (uid, uname) = (u.id, u.name) - u.name = util.NON_EXISTING_KEY + u.login = util.NON_EXISTING_KEY u.id = util.NON_EXISTING_KEY with pytest.raises(exceptions.ObjectNotFound): gr.add_user(u) - (u.name, u.id) = (uid, uname) def test_remove_non_existing_user(get_test_group: Generator[groups.Group], get_test_user: Generator[users.User]) -> None: util.start_logging() gr = get_test_group u = get_test_user - with pytest.raises(exceptions.ObjectNotFound): - gr.remove_user(u) + gr.remove_user(u) gr.add_user(u) u.id = util.NON_EXISTING_KEY u.login = util.NON_EXISTING_KEY @@ -123,6 +124,10 @@ def test_to_json(get_test_group: Generator[groups.Group]) -> None: if util.SQ.version() >= (10, 4, 0): assert "id" in gr.to_json(True) + sonar_users = groups.Group.get_object(util.SQ, "sonar-users") + json_exp = sonar_users.to_json() + assert "default" in json_exp + def test_import() -> None: data = {} @@ -164,3 +169,23 @@ def test_create_or_update(get_test_group: Generator[groups.Group]) -> None: gr2 = groups.create_or_update(util.SQ, gr.name, "Some new group description") assert gr2 is gr assert gr.description == "Some new group description" + + +def test_api_params(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + if util.SQ.version() >= (10, 4, 0): + assert gr.api_params(c.GET) == {} + assert gr.api_params(c.CREATE) == {} + else: + assert gr.api_params(c.GET) == {"name": util.TEMP_KEY} + assert gr.api_params(c.CREATE) == {"name": util.TEMP_KEY} + + +def test_get_from_id(get_test_group: Generator[groups.Group]) -> None: + gr = get_test_group + if util.SQ.version() >= (10, 4, 0): + gr2 = groups.get_object_from_id(util.SQ, gr.id) + assert gr2 is gr + else: + with pytest.raises(exceptions.UnsupportedOperation): + _ = groups.get_object_from_id(util.SQ, gr.id) diff --git a/test/unit/test_users.py b/test/unit/test_users.py index 6f44d0ac1..9f982438e 100644 --- a/test/unit/test_users.py +++ b/test/unit/test_users.py @@ -177,29 +177,32 @@ def test_update(get_test_user: Generator[users.User]) -> None: user.update(groups=["sonar-administrators"]) assert sorted(user.groups()) == ["sonar-administrators", "sonar-users"] - assert user.scm_accounts is None + assert user.scm_accounts == [] user.update(scmAccounts=["foo@gmail.com", "bar@gmail.com", "foo", "bar"]) assert sorted(user.scm_accounts) == sorted(["foo@gmail.com", "bar@gmail.com", "foo", "bar"]) - user.update(login="johndoe") - assert user.login == "johndoe" + if util.SQ.version() >= (10, 4, 0): + user.update(login="johndoe") + assert user.login == "johndoe" user.update(name="John Doe", email="john@doe.com") assert user.name == "John Doe" assert user.email == "john@doe.com" - user.update(login="jdoe", email="john@doe.com") - assert user.login == "jdoe" + if util.SQ.version() >= (10, 4, 0): + user.update(login="jdoe", email="john@doe.com") + assert user.login == "jdoe" def test_set_groups(get_test_user: Generator[users.User]) -> None: + """test_set_groups""" user = get_test_user user.set_groups(["sonar-administrators", "language-experts"]) - assert sorted(user.groups()) == sorted(["sonar-administrators", "language-experts"]) + assert sorted(user.groups()) == sorted(["sonar-users", "sonar-administrators", "language-experts"]) user.set_groups(["language-experts", "security-auditors", "developers"]) - assert sorted(user.groups()) == sorted(["language-experts", "security-auditors", "developers"]) + assert sorted(user.groups()) == sorted(["sonar-users", "language-experts", "security-auditors", "developers"]) def test_import() -> None: @@ -221,7 +224,7 @@ def test_import() -> None: for uname in "TEMP", "TEMP_ADMIN": assert users.exists(endpoint=util.SQ, login=uname) o_g = users.User.get_object(endpoint=util.SQ, login=uname) - assert o_g.description == f"User name {uname}" + assert o_g.name == f"User name {uname}" o_g.delete() From 17c9183761b0d0d44fc468e07aec1c93d9644026 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 3 Jan 2025 18:56:14 +0100 Subject: [PATCH 19/22] Update copyright to 2025 (#1553) --- README.md | 2 +- cli/__init__.py | 2 +- cli/audit.py | 2 +- cli/config.py | 2 +- cli/cust_measures.py | 2 +- cli/findings_export.py | 2 +- cli/findings_sync.py | 2 +- cli/housekeeper.py | 2 +- cli/loc.py | 2 +- cli/measures_export.py | 2 +- cli/options.py | 2 +- cli/projects_cli.py | 2 +- cli/projects_export.py | 2 +- cli/projects_import.py | 2 +- cli/rules_cli.py | 2 +- cli/support.py | 2 +- conf/build.sh | 2 +- conf/deploy.bat | 2 +- conf/deploy.sh | 2 +- conf/release.sh | 2 +- conf/run_linters.sh | 2 +- conf/run_tests.sh | 2 +- conf/scan.sh | 2 +- conf/shellcheck2sonar.py | 2 +- conf/trivy2sonar.py | 2 +- doc/api/source/conf.py | 2 +- doc/sonar-audit.md | 2 +- doc/sonar-findings-sync.md | 2 +- migration/README.md | 2 +- migration/__init__.py | 2 +- migration/deploy.sh | 2 +- migration/migration.py | 2 +- migration/sonar_migration | 4 ++-- setup.py | 2 +- setup_migration.py | 2 +- sonar-tools | 3 ++- sonar/__init__.py | 2 +- sonar/aggregations.py | 2 +- sonar/app_branches.py | 2 +- sonar/applications.py | 2 +- sonar/audit/__init__.py | 2 +- sonar/audit/config.py | 2 +- sonar/audit/problem.py | 2 +- sonar/audit/rules.py | 2 +- sonar/audit/severities.py | 2 +- sonar/audit/sonar-audit.properties | 2 +- sonar/audit/types.py | 2 +- sonar/branches.py | 2 +- sonar/changelog.py | 2 +- sonar/components.py | 2 +- sonar/custom_measures.py | 2 +- sonar/dce/__init__.py | 2 +- sonar/dce/app_nodes.py | 2 +- sonar/dce/nodes.py | 2 +- sonar/dce/search_nodes.py | 2 +- sonar/devops.py | 2 +- sonar/errcodes.py | 2 +- sonar/exceptions.py | 2 +- sonar/findings.py | 2 +- sonar/groups.py | 2 +- sonar/hotspots.py | 2 +- sonar/issues.py | 2 +- sonar/languages.py | 2 +- sonar/logging.py | 2 +- sonar/measures.py | 2 +- sonar/metrics.py | 2 +- sonar/organizations.py | 2 +- sonar/permissions/__init__.py | 2 +- sonar/permissions/aggregation_permissions.py | 2 +- sonar/permissions/application_permissions.py | 2 +- sonar/permissions/global_permissions.py | 2 +- sonar/permissions/permission_templates.py | 2 +- sonar/permissions/permissions.py | 2 +- sonar/permissions/portfolio_permissions.py | 2 +- sonar/permissions/project_permissions.py | 2 +- sonar/permissions/quality_permissions.py | 2 +- sonar/permissions/qualitygate_permissions.py | 2 +- sonar/permissions/qualityprofile_permissions.py | 2 +- sonar/permissions/template_permissions.py | 2 +- sonar/platform.py | 2 +- sonar/portfolio_reference.py | 2 +- sonar/portfolios.py | 2 +- sonar/projects.py | 2 +- sonar/pull_requests.py | 2 +- sonar/qualitygates.py | 2 +- sonar/qualityprofiles.py | 2 +- sonar/rules.py | 2 +- sonar/settings.py | 2 +- sonar/sif.py | 2 +- sonar/sif_node.py | 2 +- sonar/sqobject.py | 2 +- sonar/syncer.py | 2 +- sonar/tasks.py | 2 +- sonar/tokens.py | 2 +- sonar/users.py | 2 +- sonar/util/__init__.py | 2 +- sonar/util/cache.py | 2 +- sonar/util/constants.py | 2 +- sonar/util/sonar_cache.py | 2 +- sonar/util/types.py | 2 +- sonar/utilities.py | 2 +- sonar/version.py | 2 +- sonar/webhooks.py | 2 +- sync-issues.properties | 2 +- test/.sonar-audit.properties | 2 +- test/integration/file-with-ambiguous-issues.py | 2 +- test/integration/it-docker.sh | 2 +- test/integration/it-tools.sh | 2 +- test/integration/it.sh | 2 +- test/sonar-test.sh | 2 +- test/unit/conftest.py | 2 +- test/unit/test_apps.py | 2 +- test/unit/test_audit.py | 2 +- test/unit/test_config.py | 2 +- test/unit/test_devops.py | 2 +- test/unit/test_findings.py | 2 +- test/unit/test_findings_sync.py | 2 +- test/unit/test_groups.py | 2 +- test/unit/test_housekeeper.py | 2 +- test/unit/test_issues.py | 2 +- test/unit/test_loc.py | 2 +- test/unit/test_logging.py | 2 +- test/unit/test_measures.py | 2 +- test/unit/test_migration.py | 2 +- test/unit/test_portfolios.py | 2 +- test/unit/test_project_export.py | 2 +- test/unit/test_projects.py | 2 +- test/unit/test_qp.py | 2 +- test/unit/test_rules.py | 2 +- test/unit/test_sif.py | 2 +- test/unit/test_sonarcloud.py | 2 +- test/unit/test_sqobject.py | 2 +- test/unit/test_tasks.py | 2 +- test/unit/test_users.py | 2 +- test/unit/utilities.py | 2 +- 135 files changed, 137 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 1834a0fe6..4c91eb120 100644 --- a/README.md +++ b/README.md @@ -379,7 +379,7 @@ When tools complete successfully they return exit code 0. En case of fatal error # License -Copyright (C) 2019-2024 Olivier Korach +Copyright (C) 2019-2025 Olivier Korach mailto:olivier.korach AT gmail DOT com This program is free software; you can redistribute it and/or diff --git a/cli/__init__.py b/cli/__init__.py index 205618ccc..c14520d5e 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/audit.py b/cli/audit.py index 78f05e235..4201452e2 100755 --- a/cli/audit.py +++ b/cli/audit.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/config.py b/cli/config.py index 5c2c5d9a1..6b885f4c2 100644 --- a/cli/config.py +++ b/cli/config.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/cust_measures.py b/cli/cust_measures.py index ed58426b5..d903a2e7f 100644 --- a/cli/cust_measures.py +++ b/cli/cust_measures.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/findings_export.py b/cli/findings_export.py index 03237ca8e..dbc7b56b2 100755 --- a/cli/findings_export.py +++ b/cli/findings_export.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/findings_sync.py b/cli/findings_sync.py index fb08efef3..801ecf5fb 100755 --- a/cli/findings_sync.py +++ b/cli/findings_sync.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/housekeeper.py b/cli/housekeeper.py index 55abb02df..436e7fc92 100644 --- a/cli/housekeeper.py +++ b/cli/housekeeper.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/loc.py b/cli/loc.py index 2bb5bb44d..c7e27fd5f 100644 --- a/cli/loc.py +++ b/cli/loc.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/measures_export.py b/cli/measures_export.py index a0619ee86..35b3a165d 100755 --- a/cli/measures_export.py +++ b/cli/measures_export.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/options.py b/cli/options.py index 9519c5bad..a0859a92d 100644 --- a/cli/options.py +++ b/cli/options.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/projects_cli.py b/cli/projects_cli.py index 6e5896b9f..b8e0f4167 100644 --- a/cli/projects_cli.py +++ b/cli/projects_cli.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/projects_export.py b/cli/projects_export.py index 5724f57b8..41dd9fd7b 100755 --- a/cli/projects_export.py +++ b/cli/projects_export.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/projects_import.py b/cli/projects_import.py index ac0b238a6..ed87040eb 100755 --- a/cli/projects_import.py +++ b/cli/projects_import.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/rules_cli.py b/cli/rules_cli.py index a4e6d3673..3cf4ea838 100755 --- a/cli/rules_cli.py +++ b/cli/rules_cli.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/cli/support.py b/cli/support.py index 4f9d8adae..76693f99d 100755 --- a/cli/support.py +++ b/cli/support.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/build.sh b/conf/build.sh index 2a9bcfa88..d921d4d7a 100755 --- a/conf/build.sh +++ b/conf/build.sh @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/deploy.bat b/conf/deploy.bat index 605a9ba31..b9b3b3eb8 100644 --- a/conf/deploy.bat +++ b/conf/deploy.bat @@ -1,6 +1,6 @@ :: sonar-tools -:: Copyright (C) 2022-2024 Olivier Korach +:: Copyright (C) 2022-2025 Olivier Korach :: mailto:olivier.korach AT gmail DOT com :: :: This program is free software; you can redistribute it and/or diff --git a/conf/deploy.sh b/conf/deploy.sh index dca810bba..4435d25f1 100755 --- a/conf/deploy.sh +++ b/conf/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/release.sh b/conf/release.sh index 6ef0008cd..15e8e5904 100755 --- a/conf/release.sh +++ b/conf/release.sh @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/run_linters.sh b/conf/run_linters.sh index bbd35a619..0bbf825f4 100755 --- a/conf/run_linters.sh +++ b/conf/run_linters.sh @@ -1,7 +1,7 @@ #!/bin/bash # # media-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/run_tests.sh b/conf/run_tests.sh index 27a87b6ed..32b7e7594 100755 --- a/conf/run_tests.sh +++ b/conf/run_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/scan.sh b/conf/scan.sh index e22f7c30b..d49f68275 100755 --- a/conf/scan.sh +++ b/conf/scan.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/shellcheck2sonar.py b/conf/shellcheck2sonar.py index 17a5ae955..59fe93f7c 100755 --- a/conf/shellcheck2sonar.py +++ b/conf/shellcheck2sonar.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/conf/trivy2sonar.py b/conf/trivy2sonar.py index 9001e454d..1e62c64d0 100755 --- a/conf/trivy2sonar.py +++ b/conf/trivy2sonar.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/doc/api/source/conf.py b/doc/api/source/conf.py index c3ba8a638..6ff4ffccd 100644 --- a/doc/api/source/conf.py +++ b/doc/api/source/conf.py @@ -23,7 +23,7 @@ # -- Project information ----------------------------------------------------- project = "sonar-tools" -copyright = "2022-2024, Olivier Korach" +copyright = "2019-2025, Olivier Korach" author = "Olivier Korach" # The full version, including alpha/beta/rc tags diff --git a/doc/sonar-audit.md b/doc/sonar-audit.md index f889f89fb..c85d2e961 100644 --- a/doc/sonar-audit.md +++ b/doc/sonar-audit.md @@ -220,7 +220,7 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';' # License -Copyright (C) 2019-2024 Olivier Korach +Copyright (C) 2019-2025 Olivier Korach mailto:olivier.korach AT gmail DOT com This program is free software; you can redistribute it and/or diff --git a/doc/sonar-findings-sync.md b/doc/sonar-findings-sync.md index c8a1359ba..21cd944d5 100644 --- a/doc/sonar-findings-sync.md +++ b/doc/sonar-findings-sync.md @@ -119,7 +119,7 @@ sonar-findings-sync -k myPorjectKey -U https://anothersonar.acme-corp.com -t d04 # License -Copyright (C) 2019-2024 Olivier Korach +Copyright (C) 2019-2025 Olivier Korach mailto:olivier.korach AT gmail DOT com This program is free software; you can redistribute it and/or diff --git a/migration/README.md b/migration/README.md index 08f895926..e46acc3ef 100644 --- a/migration/README.md +++ b/migration/README.md @@ -167,7 +167,7 @@ When sonar-migration complete successfully they return exit code 0. En case of f # License -Copyright (C) 2024 Olivier Korach +Copyright (C) 2024-2025 Olivier Korach mailto:olivier.korach AT gmail DOT com This program is free software; you can redistribute it and/or diff --git a/migration/__init__.py b/migration/__init__.py index 205618ccc..4d72f740c 100644 --- a/migration/__init__.py +++ b/migration/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/migration/deploy.sh b/migration/deploy.sh index c00620266..c5d0dd92e 100755 --- a/migration/deploy.sh +++ b/migration/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/migration/migration.py b/migration/migration.py index f6d17ed2c..eb2a8b622 100644 --- a/migration/migration.py +++ b/migration/migration.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/migration/sonar_migration b/migration/sonar_migration index 068b0560c..103d82301 100755 --- a/migration/sonar_migration +++ b/migration/sonar_migration @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or @@ -25,7 +25,7 @@ from sonar import version print(f''' sonar-migration version {version.PACKAGE_VERSION} - +(c) Olivier Korach 2024-2025 run: sonar-migration -u -t See tools built-in -h help and https://github.com/okorach/sonar-tools/migration/README.md for more documentation diff --git a/setup.py b/setup.py index 18a6b4bf0..d583b6024 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/setup_migration.py b/setup_migration.py index d60fdef03..b26541c37 100644 --- a/setup_migration.py +++ b/setup_migration.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar-tools b/sonar-tools index 0cf9132f3..e83f8a27f 100755 --- a/sonar-tools +++ b/sonar-tools @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or @@ -25,6 +25,7 @@ from sonar import version print(f''' sonar-tools version {version.PACKAGE_VERSION} +(c) Olivier Korach 2019-2025 Collections of utilities for SonarQube and SonarCloud: - sonar-audit: Audits a SonarQube/SonarCloud platform for bad practices, performance, configuration problems - sonar-housekeeper: Deletes projects that have not been analyzed since a given number of days diff --git a/sonar/__init__.py b/sonar/__init__.py index 5204d9bc5..9d70c1618 100644 --- a/sonar/__init__.py +++ b/sonar/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/aggregations.py b/sonar/aggregations.py index d2d362bf4..e660d8fd3 100644 --- a/sonar/aggregations.py +++ b/sonar/aggregations.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/app_branches.py b/sonar/app_branches.py index 05f135971..77cc979a5 100644 --- a/sonar/app_branches.py +++ b/sonar/app_branches.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/applications.py b/sonar/applications.py index 4c930cffa..95ec84e6b 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/__init__.py b/sonar/audit/__init__.py index b575855b8..447dc7f90 100644 --- a/sonar/audit/__init__.py +++ b/sonar/audit/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/config.py b/sonar/audit/config.py index d7a905fcb..47a42fdef 100644 --- a/sonar/audit/config.py +++ b/sonar/audit/config.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/problem.py b/sonar/audit/problem.py index 66af356e7..1d3a78a63 100644 --- a/sonar/audit/problem.py +++ b/sonar/audit/problem.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/rules.py b/sonar/audit/rules.py index 6da563707..3c4fb3e4f 100644 --- a/sonar/audit/rules.py +++ b/sonar/audit/rules.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/severities.py b/sonar/audit/severities.py index 9289f1ebc..c02153f8f 100644 --- a/sonar/audit/severities.py +++ b/sonar/audit/severities.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/sonar-audit.properties b/sonar/audit/sonar-audit.properties index e7b96d09c..2cb588c93 100644 --- a/sonar/audit/sonar-audit.properties +++ b/sonar/audit/sonar-audit.properties @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/audit/types.py b/sonar/audit/types.py index 81217179e..87eb09e44 100644 --- a/sonar/audit/types.py +++ b/sonar/audit/types.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/branches.py b/sonar/branches.py index 639cd6b12..5e2d6aba2 100644 --- a/sonar/branches.py +++ b/sonar/branches.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/changelog.py b/sonar/changelog.py index 6cb09a079..10c717a84 100644 --- a/sonar/changelog.py +++ b/sonar/changelog.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/components.py b/sonar/components.py index 8a52621c8..80e0ba664 100644 --- a/sonar/components.py +++ b/sonar/components.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/custom_measures.py b/sonar/custom_measures.py index c69749cf7..9556dbc92 100644 --- a/sonar/custom_measures.py +++ b/sonar/custom_measures.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/dce/__init__.py b/sonar/dce/__init__.py index 205618ccc..c14520d5e 100644 --- a/sonar/dce/__init__.py +++ b/sonar/dce/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/dce/app_nodes.py b/sonar/dce/app_nodes.py index 9e424350a..e66a0474e 100644 --- a/sonar/dce/app_nodes.py +++ b/sonar/dce/app_nodes.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/dce/nodes.py b/sonar/dce/nodes.py index cb3a7af29..c965f0e98 100644 --- a/sonar/dce/nodes.py +++ b/sonar/dce/nodes.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/dce/search_nodes.py b/sonar/dce/search_nodes.py index 4c08504d2..1114c1c31 100644 --- a/sonar/dce/search_nodes.py +++ b/sonar/dce/search_nodes.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/devops.py b/sonar/devops.py index 0d78a3cf4..debb5cf25 100644 --- a/sonar/devops.py +++ b/sonar/devops.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/errcodes.py b/sonar/errcodes.py index c208e5769..2d7b9c6c2 100644 --- a/sonar/errcodes.py +++ b/sonar/errcodes.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/exceptions.py b/sonar/exceptions.py index 26c3f846b..f5edd4523 100644 --- a/sonar/exceptions.py +++ b/sonar/exceptions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/findings.py b/sonar/findings.py index cbb22fadc..40751d252 100644 --- a/sonar/findings.py +++ b/sonar/findings.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/groups.py b/sonar/groups.py index 6d3d959ce..e255bc090 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/hotspots.py b/sonar/hotspots.py index 679cdacad..6c60d0387 100644 --- a/sonar/hotspots.py +++ b/sonar/hotspots.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/issues.py b/sonar/issues.py index d187db983..7c21a152e 100644 --- a/sonar/issues.py +++ b/sonar/issues.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/languages.py b/sonar/languages.py index 299d1d4c7..7db971b84 100644 --- a/sonar/languages.py +++ b/sonar/languages.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/logging.py b/sonar/logging.py index 6a4c4d127..265521757 100644 --- a/sonar/logging.py +++ b/sonar/logging.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/measures.py b/sonar/measures.py index 7e90924f0..82b8650ea 100644 --- a/sonar/measures.py +++ b/sonar/measures.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/metrics.py b/sonar/metrics.py index 60ca2f173..348307ab9 100644 --- a/sonar/metrics.py +++ b/sonar/metrics.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/organizations.py b/sonar/organizations.py index 46e7e2a13..3b78b9b78 100644 --- a/sonar/organizations.py +++ b/sonar/organizations.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/__init__.py b/sonar/permissions/__init__.py index 205618ccc..c14520d5e 100644 --- a/sonar/permissions/__init__.py +++ b/sonar/permissions/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/aggregation_permissions.py b/sonar/permissions/aggregation_permissions.py index 90d914c1f..9e4dbf516 100644 --- a/sonar/permissions/aggregation_permissions.py +++ b/sonar/permissions/aggregation_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/application_permissions.py b/sonar/permissions/application_permissions.py index f30d3b0a5..69b99d1fa 100644 --- a/sonar/permissions/application_permissions.py +++ b/sonar/permissions/application_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/global_permissions.py b/sonar/permissions/global_permissions.py index 2328bdacd..e53f63808 100644 --- a/sonar/permissions/global_permissions.py +++ b/sonar/permissions/global_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/permission_templates.py b/sonar/permissions/permission_templates.py index 91844fc14..9fe1753f6 100644 --- a/sonar/permissions/permission_templates.py +++ b/sonar/permissions/permission_templates.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/permissions.py b/sonar/permissions/permissions.py index 865de1539..142bcbbe9 100644 --- a/sonar/permissions/permissions.py +++ b/sonar/permissions/permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/portfolio_permissions.py b/sonar/permissions/portfolio_permissions.py index ece931410..b4f1e33e1 100644 --- a/sonar/permissions/portfolio_permissions.py +++ b/sonar/permissions/portfolio_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/project_permissions.py b/sonar/permissions/project_permissions.py index f7434fad8..82fd68d24 100644 --- a/sonar/permissions/project_permissions.py +++ b/sonar/permissions/project_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/quality_permissions.py b/sonar/permissions/quality_permissions.py index 845731f45..3f1414985 100644 --- a/sonar/permissions/quality_permissions.py +++ b/sonar/permissions/quality_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/qualitygate_permissions.py b/sonar/permissions/qualitygate_permissions.py index 81f54eed0..4d2ca77ae 100644 --- a/sonar/permissions/qualitygate_permissions.py +++ b/sonar/permissions/qualitygate_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/qualityprofile_permissions.py b/sonar/permissions/qualityprofile_permissions.py index 585ced7b6..c64e4373f 100644 --- a/sonar/permissions/qualityprofile_permissions.py +++ b/sonar/permissions/qualityprofile_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/permissions/template_permissions.py b/sonar/permissions/template_permissions.py index 99ad479f3..428524299 100644 --- a/sonar/permissions/template_permissions.py +++ b/sonar/permissions/template_permissions.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/platform.py b/sonar/platform.py index 33f49c52f..51467bc0c 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/portfolio_reference.py b/sonar/portfolio_reference.py index a6d2d2dc6..e6c8219b4 100644 --- a/sonar/portfolio_reference.py +++ b/sonar/portfolio_reference.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 28643b7c4..791e75960 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/projects.py b/sonar/projects.py index 5381f539c..badc58774 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/pull_requests.py b/sonar/pull_requests.py index 0bbc717d4..a363c986c 100644 --- a/sonar/pull_requests.py +++ b/sonar/pull_requests.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index aa71a4fe0..8e1391461 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index e53631e0f..3eec17a3d 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/rules.py b/sonar/rules.py index 128a82c29..cdab6e950 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/settings.py b/sonar/settings.py index 9db5433a8..cd04dfe1e 100644 --- a/sonar/settings.py +++ b/sonar/settings.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/sif.py b/sonar/sif.py index df7ed4696..b8ce8f22b 100644 --- a/sonar/sif.py +++ b/sonar/sif.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/sif_node.py b/sonar/sif_node.py index 6ce943911..fdabb1255 100644 --- a/sonar/sif_node.py +++ b/sonar/sif_node.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/sqobject.py b/sonar/sqobject.py index c21a557a5..91b3d37c4 100644 --- a/sonar/sqobject.py +++ b/sonar/sqobject.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/syncer.py b/sonar/syncer.py index 4e0f4f32b..064623c14 100644 --- a/sonar/syncer.py +++ b/sonar/syncer.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/tasks.py b/sonar/tasks.py index d295cfde3..a9835a292 100644 --- a/sonar/tasks.py +++ b/sonar/tasks.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/tokens.py b/sonar/tokens.py index b172a4f66..22701820a 100644 --- a/sonar/tokens.py +++ b/sonar/tokens.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/users.py b/sonar/users.py index 42b269f20..38cbe7deb 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/util/__init__.py b/sonar/util/__init__.py index d4dcacc89..9d70c1618 100644 --- a/sonar/util/__init__.py +++ b/sonar/util/__init__.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/util/cache.py b/sonar/util/cache.py index 4fba33e07..cc010a728 100644 --- a/sonar/util/cache.py +++ b/sonar/util/cache.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/util/constants.py b/sonar/util/constants.py index 65ee22396..31e52c3f8 100644 --- a/sonar/util/constants.py +++ b/sonar/util/constants.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/util/sonar_cache.py b/sonar/util/sonar_cache.py index 4d0aadc87..cd036e307 100644 --- a/sonar/util/sonar_cache.py +++ b/sonar/util/sonar_cache.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/util/types.py b/sonar/util/types.py index bea709ab3..1f258300a 100644 --- a/sonar/util/types.py +++ b/sonar/util/types.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/utilities.py b/sonar/utilities.py index c9c3a52cb..12cdfd150 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/version.py b/sonar/version.py index ebe4e2fcf..22258a3e2 100644 --- a/sonar/version.py +++ b/sonar/version.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sonar/webhooks.py b/sonar/webhooks.py index a6c811b29..dedf28699 100644 --- a/sonar/webhooks.py +++ b/sonar/webhooks.py @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/sync-issues.properties b/sync-issues.properties index 94d5f0d0f..7b19413ad 100644 --- a/sync-issues.properties +++ b/sync-issues.properties @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/.sonar-audit.properties b/test/.sonar-audit.properties index 637deb8dc..9866d38ce 100644 --- a/test/.sonar-audit.properties +++ b/test/.sonar-audit.properties @@ -1,6 +1,6 @@ # # sonar-tools -# Copyright (C) 2019-2024 Olivier Korach +# Copyright (C) 2019-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/integration/file-with-ambiguous-issues.py b/test/integration/file-with-ambiguous-issues.py index 0a5941f87..1207fc2f3 100644 --- a/test/integration/file-with-ambiguous-issues.py +++ b/test/integration/file-with-ambiguous-issues.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/integration/it-docker.sh b/test/integration/it-docker.sh index 2ab6d9567..5832503c5 100755 --- a/test/integration/it-docker.sh +++ b/test/integration/it-docker.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2021-2024 Olivier Korach +# Copyright (C) 2021-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/integration/it-tools.sh b/test/integration/it-tools.sh index 8aa37b1de..93c085744 100644 --- a/test/integration/it-tools.sh +++ b/test/integration/it-tools.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2021-2024 Olivier Korach +# Copyright (C) 2021-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/integration/it.sh b/test/integration/it.sh index 657721f5d..808944529 100755 --- a/test/integration/it.sh +++ b/test/integration/it.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2021-2024 Olivier Korach +# Copyright (C) 2021-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/sonar-test.sh b/test/sonar-test.sh index 43db829b4..96804806a 100755 --- a/test/sonar-test.sh +++ b/test/sonar-test.sh @@ -1,7 +1,7 @@ #!/bin/bash # # sonar-tools -# Copyright (C) 2022-2024 Olivier Korach +# Copyright (C) 2022-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/conftest.py b/test/unit/conftest.py index c57e4bed4..6ae79a994 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_apps.py b/test/unit/test_apps.py index ca35c2d7e..11650b5fd 100644 --- a/test/unit/test_apps.py +++ b/test/unit/test_apps.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_audit.py b/test/unit/test_audit.py index 554f3f1f3..1785e5b95 100644 --- a/test/unit/test_audit.py +++ b/test/unit/test_audit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 2ce63dc8f..6456079fe 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_devops.py b/test/unit/test_devops.py index 8d531a8d7..fce291871 100644 --- a/test/unit/test_devops.py +++ b/test/unit/test_devops.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_findings.py b/test/unit/test_findings.py index 036febbb4..6ffe023f9 100644 --- a/test/unit/test_findings.py +++ b/test/unit/test_findings.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_findings_sync.py b/test/unit/test_findings_sync.py index 6d67d4d06..f7e92f7f3 100644 --- a/test/unit/test_findings_sync.py +++ b/test/unit/test_findings_sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_groups.py b/test/unit/test_groups.py index d48880d95..72da21434 100644 --- a/test/unit/test_groups.py +++ b/test/unit/test_groups.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_housekeeper.py b/test/unit/test_housekeeper.py index 4fec02c6d..32fdf87e3 100644 --- a/test/unit/test_housekeeper.py +++ b/test/unit/test_housekeeper.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_issues.py b/test/unit/test_issues.py index bb47fb952..4f3b0e166 100644 --- a/test/unit/test_issues.py +++ b/test/unit/test_issues.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_loc.py b/test/unit/test_loc.py index 40bb5ce6e..c4e485ac2 100644 --- a/test/unit/test_loc.py +++ b/test/unit/test_loc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_logging.py b/test/unit/test_logging.py index 27ffd8ef6..e7dfbbd0c 100644 --- a/test/unit/test_logging.py +++ b/test/unit/test_logging.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_measures.py b/test/unit/test_measures.py index 406d0eaa7..058c3bae3 100644 --- a/test/unit/test_measures.py +++ b/test/unit/test_measures.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_migration.py b/test/unit/test_migration.py index a55c7a65f..654451a54 100644 --- a/test/unit/test_migration.py +++ b/test/unit/test_migration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-migration tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_portfolios.py b/test/unit/test_portfolios.py index 0abe8e483..edc003ba1 100644 --- a/test/unit/test_portfolios.py +++ b/test/unit/test_portfolios.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_project_export.py b/test/unit/test_project_export.py index ffe35ea06..b98c473e6 100644 --- a/test/unit/test_project_export.py +++ b/test/unit/test_project_export.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_projects.py b/test/unit/test_projects.py index 424dc9c9f..1c9aefb87 100644 --- a/test/unit/test_projects.py +++ b/test/unit/test_projects.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_qp.py b/test/unit/test_qp.py index a6dc38300..8b7b4b927 100644 --- a/test/unit/test_qp.py +++ b/test/unit/test_qp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_rules.py b/test/unit/test_rules.py index 1536c2c3b..3ec90c464 100644 --- a/test/unit/test_rules.py +++ b/test/unit/test_rules.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_sif.py b/test/unit/test_sif.py index dad1cfb80..6f87340d9 100644 --- a/test/unit/test_sif.py +++ b/test/unit/test_sif.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_sonarcloud.py b/test/unit/test_sonarcloud.py index c13c99bee..d36c52182 100644 --- a/test/unit/test_sonarcloud.py +++ b/test/unit/test_sonarcloud.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_sqobject.py b/test/unit/test_sqobject.py index 7bac88daa..6e28b78fe 100644 --- a/test/unit/test_sqobject.py +++ b/test/unit/test_sqobject.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_tasks.py b/test/unit/test_tasks.py index 3d20e7e19..50387f140 100644 --- a/test/unit/test_tasks.py +++ b/test/unit/test_tasks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/test_users.py b/test/unit/test_users.py index 9f982438e..c3a4b048d 100644 --- a/test/unit/test_users.py +++ b/test/unit/test_users.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or diff --git a/test/unit/utilities.py b/test/unit/utilities.py index fd51f499d..cd98d426f 100644 --- a/test/unit/utilities.py +++ b/test/unit/utilities.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # sonar-tools tests -# Copyright (C) 2024 Olivier Korach +# Copyright (C) 2024-2025 Olivier Korach # mailto:olivier.korach AT gmail DOT com # # This program is free software; you can redistribute it and/or From 0f25f5ddd7dac4cc6bc332f566825d1d298c3111 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Sun, 5 Jan 2025 17:52:37 +0100 Subject: [PATCH 20/22] projects_cli tests (#1554) * Improve import code * Make dict index resilient on export_settings * Remove useless imports * Add projects_cli tests * More tests * Formatting * Fix tests * Improve tests * Adjust config to better point at tests * Update with more recent settings * Remove useless log * New tests * More tests * More tests * Add audit test * Set sonarcloud DB to postgresql * Fix cache hash and reset_setting param * Fix tests * Fix test * Make _sys_info restricted * Fix test * Fix tests * Fixes from tests * run_around_tests() * MOve run_around_test in module * More tests * Quality pass * Quality pass --- cli/projects_cli.py | 27 +- sonar-project.properties | 11 +- sonar/issues.py | 35 +- sonar/platform.py | 22 +- sonar/settings.py | 4 +- test/files/config.json | 2180 ++++++------------------------ test/unit/conftest.py | 8 + test/unit/test_cli.py | 86 ++ test/unit/test_groups.py | 5 +- test/unit/test_issues.py | 79 +- test/unit/test_platform.py | 104 ++ test/unit/test_project_export.py | 21 +- test/unit/test_sonarcloud.py | 5 + 13 files changed, 743 insertions(+), 1844 deletions(-) create mode 100644 test/unit/test_cli.py create mode 100644 test/unit/test_platform.py diff --git a/cli/projects_cli.py b/cli/projects_cli.py index b8e0f4167..5141edba1 100644 --- a/cli/projects_cli.py +++ b/cli/projects_cli.py @@ -53,21 +53,16 @@ def __export_projects(endpoint: platform.Platform, **kwargs) -> None: def __check_sq_environments(import_sq: platform.Platform, export_sq: dict[str, str]) -> None: """Checks if export and import environments are compatibles""" - imp_version = utilities.version_to_string(import_sq.version(digits=2)) - if imp_version != export_sq["version"]: - raise exceptions.UnsupportedOperation("Export was not performed with same SonarQube version, aborting...") - for export_plugin in export_sq["plugins"]: - e_name = export_plugin["name"] - e_vers = export_plugin["version"] - found = False - for import_plugin in import_sq.plugins(): - if import_plugin["name"] == e_name and import_plugin["version"] == e_vers: - found = True - break - if not found: - raise exceptions.UnsupportedOperation( - f"Plugin '{e_name}' version '{e_vers}' was not found or not in same version on import platform, aborting..." - ) + imp_version = import_sq.version()[:2] + exp_version = tuple(int(n) for n in export_sq["version"].split(".")[:2]) + if imp_version != exp_version: + raise exceptions.UnsupportedOperation( + f"Export was not performed with same SonarQube version, aborting... ({utilities.version_to_string(exp_version)} vs {utilities.version_to_string(imp_version)})" + ) + if export_sq["plugins"] != import_sq.plugins(): + raise exceptions.UnsupportedOperation( + f"Plugin list is different on the import and export platforms ({str(export_sq['plugins'])} vs {str(import_sq.plugins())}), aborting..." + ) def __import_projects(endpoint: platform.Platform, **kwargs) -> None: @@ -99,7 +94,7 @@ def __import_projects(endpoint: platform.Platform, **kwargs) -> None: else: statuses[s] = 1 i += 1 - log.info("%d/%d exports (%d%%) - Latest: %s - %s", i, nb_projects, int(i * 100 / nb_projects), project["key"], status) + log.info("%d/%d exports (%d%%) - Latest: %s - %s", i, nb_projects, int(i * 100 / nb_projects), project["key"], s) log.info("%s", ", ".join([f"{k}:{v}" for k, v in statuses.items()])) diff --git a/sonar-project.properties b/sonar-project.properties index d6ef40e91..a21531f60 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,20 +1,21 @@ sonar.organization=okorach sonar.projectKey=okorach_sonar-tools sonar.projectName=Sonar Tools +sonar.python.version=3.9 # Comma-separated paths to directories with sources (required) -sonar.sources=. +sonar.sources=sonar, cli, migration, setup.py, setup_migration.py + # Encoding of the source files sonar.sourceEncoding=UTF-8 -sonar.exclusions=**/*.csv, **/*.xls*, tests-it/*, tmp/*, **/*.json -sonar.python.version=3.9 +sonar.exclusions=doc/api/**/*, build/**/* sonar.python.flake8.reportPaths=build/flake8-report.out sonar.python.pylint.reportPaths=build/pylint-report.out sonar.sarifReportPaths=build/results_sarif.sarif # sonar.externalIssuesReportPaths=build/shellcheck.json,build/trivy.json # sonar.python.bandit.reportPaths=build/bandit-report.json -sonar.exclusions=doc/api/**/*, build/**/*, test/**/* +sonar.tests=test/latest, test/lts sonar.coverage.exclusions=setup*.py, test/**/*, conf/*2sonar.py, cli/cust_measures.py, sonar/custom_measures.py, cli/support.py, cli/projects_export.py, cli/projects_import.py + sonar.cpd.exclusions=setup*.py -sonar.tests=test/unit diff --git a/sonar/issues.py b/sonar/issues.py index 7c21a152e..aa26ddde2 100644 --- a/sonar/issues.py +++ b/sonar/issues.py @@ -574,7 +574,7 @@ def component_filter(endpoint: pf.Platform) -> str: return COMPONENT_FILTER_OLD -def __search_all_by_directories(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: +def search_by_directory(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: """Searches issues splitting by directory to avoid exceeding the 10K limit""" new_params = params.copy() facets = _get_facets(endpoint=endpoint, project_key=new_params[component_filter(endpoint)], facets="directories", params=new_params) @@ -582,12 +582,12 @@ def __search_all_by_directories(endpoint: pf.Platform, params: ApiParams) -> dic log.info("Splitting search by directories") for d in facets["directories"]: new_params["directories"] = d["val"] - issue_list.update(search(endpoint=endpoint, params=new_params, raise_error=False)) + issue_list.update(search(endpoint=endpoint, params=new_params, raise_error=True)) log.debug("Search by directory ALL: %d issues found", len(issue_list)) return issue_list -def __search_all_by_types(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: +def search_by_type(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: """Searches issues splitting by type to avoid exceeding the 10K limit""" issue_list = {} new_params = params.copy() @@ -598,12 +598,12 @@ def __search_all_by_types(endpoint: pf.Platform, params: ApiParams) -> dict[str, issue_list.update(search(endpoint=endpoint, params=new_params)) except TooManyIssuesError: log.info(_TOO_MANY_ISSUES_MSG) - issue_list.update(__search_all_by_directories(endpoint=endpoint, params=new_params)) + issue_list.update(search_by_directory(endpoint=endpoint, params=new_params)) log.debug("Search by type ALL: %d issues found", len(issue_list)) return issue_list -def __search_all_by_severities(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: +def search_by_severity(endpoint: pf.Platform, params: ApiParams) -> dict[str, Issue]: """Searches issues splitting by severity to avoid exceeding the 10K limit""" issue_list = {} new_params = params.copy() @@ -614,14 +614,12 @@ def __search_all_by_severities(endpoint: pf.Platform, params: ApiParams) -> dict issue_list.update(search(endpoint=endpoint, params=new_params)) except TooManyIssuesError: log.info(_TOO_MANY_ISSUES_MSG) - issue_list.update(__search_all_by_types(endpoint=endpoint, params=new_params)) + issue_list.update(search_by_type(endpoint=endpoint, params=new_params)) log.debug("Search by severity ALL: %d issues found", len(issue_list)) return issue_list -def __search_all_by_date( - endpoint: pf.Platform, params: ApiParams, date_start: Optional[date] = None, date_stop: Optional[date] = None -) -> dict[str, Issue]: +def search_by_date(endpoint: pf.Platform, params: ApiParams, date_start: Optional[date] = None, date_stop: Optional[date] = None) -> dict[str, Issue]: """Searches issues splitting by date windows to avoid exceeding the 10K limit""" new_params = params.copy() if date_start is None: @@ -648,15 +646,15 @@ def __search_all_by_date( diff = (date_stop - date_start).days if diff == 0: log.info(_TOO_MANY_ISSUES_MSG) - issue_list = __search_all_by_severities(endpoint, new_params) + issue_list = search_by_severity(endpoint, new_params) elif diff == 1: - issue_list.update(__search_all_by_date(endpoint=endpoint, params=new_params, date_start=date_start, date_stop=date_start)) - issue_list.update(__search_all_by_date(endpoint=endpoint, params=new_params, date_start=date_stop, date_stop=date_stop)) + issue_list.update(search_by_date(endpoint=endpoint, params=new_params, date_start=date_start, date_stop=date_start)) + issue_list.update(search_by_date(endpoint=endpoint, params=new_params, date_start=date_stop, date_stop=date_stop)) else: date_middle = date_start + timedelta(days=diff // 2) - issue_list.update(__search_all_by_date(endpoint=endpoint, params=new_params, date_start=date_start, date_stop=date_middle)) + issue_list.update(search_by_date(endpoint=endpoint, params=new_params, date_start=date_start, date_stop=date_middle)) date_middle = date_middle + timedelta(days=1) - issue_list.update(__search_all_by_date(endpoint=endpoint, params=new_params, date_start=date_middle, date_stop=date_stop)) + issue_list.update(search_by_date(endpoint=endpoint, params=new_params, date_start=date_middle, date_stop=date_stop)) if date_start is not None and date_stop is not None: log.debug( "Project '%s' has %d issues between %s and %s", @@ -678,7 +676,7 @@ def __search_all_by_project(endpoint: pf.Platform, project_key: str, params: Api issue_list.update(search(endpoint=endpoint, params=new_params)) except TooManyIssuesError: log.info(_TOO_MANY_ISSUES_MSG) - issue_list.update(__search_all_by_date(endpoint=endpoint, params=new_params)) + issue_list.update(search_by_date(endpoint=endpoint, params=new_params)) return issue_list @@ -766,11 +764,10 @@ def search_first(endpoint: pf.Platform, **params) -> Union[Issue, None]: """ filters = pre_search_filters(endpoint=endpoint, params=params) filters["ps"] = 1 - data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text) + data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text)["issues"] if len(data) == 0: return None - i = data["issues"][0] - return get_object(endpoint=endpoint, key=i["key"], data=i) + return get_object(endpoint=endpoint, key=data[0]["key"], data=data[0]) def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool = True, threads: int = 8) -> dict[str, Issue]: @@ -821,6 +818,8 @@ def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool = def _get_facets(endpoint: pf.Platform, project_key: str, facets: str = "directories", params: ApiParams = None) -> dict[str, str]: """Returns the facets of a search""" + if not params: + params = {} params.update({component_filter(endpoint): project_key, "facets": facets, "ps": Issue.MAX_PAGE_SIZE, "additionalFields": "comments"}) filters = pre_search_filters(endpoint=endpoint, params=params) data = json.loads(endpoint.get(Issue.API[c.SEARCH], params=filters).text) diff --git a/sonar/platform.py b/sonar/platform.py index 51467bc0c..793e79ceb 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -80,7 +80,7 @@ def __init__(self, url: str, token: str, org: str = None, cert_file: Optional[st self.__cert_file = cert_file self.__user_data = None self._version = None - self.__sys_info = None + self._sys_info = None self.__global_nav = None self._server_id = None self._permissions = None @@ -147,8 +147,8 @@ def server_id(self) -> str: """ if self._server_id is not None: return self._server_id - if self.__sys_info is not None and _SERVER_ID_KEY in self.__sys_info["System"]: - self._server_id = self.__sys_info["System"][_SERVER_ID_KEY] + if self._sys_info is not None and _SERVER_ID_KEY in self._sys_info["System"]: + self._server_id = self._sys_info["System"][_SERVER_ID_KEY] else: self._server_id = json.loads(self.get("system/status").text)["id"] return self._server_id @@ -307,7 +307,7 @@ def sys_info(self) -> dict[str, any]: """ if self.is_sonarcloud(): return {"System": {_SERVER_ID_KEY: "sonarcloud"}} - if self.__sys_info is None: + if self._sys_info is None: success, counter = False, 0 while not success: try: @@ -322,9 +322,9 @@ def sys_info(self) -> dict[str, any]: else: log.error("%s while getting system info", util.error_msg(e)) raise e - self.__sys_info = json.loads(resp.text) + self._sys_info = json.loads(resp.text) success = True - return self.__sys_info + return self._sys_info def global_nav(self) -> dict[str, any]: """ @@ -340,7 +340,7 @@ def database(self) -> str: :return: the SonarQube platform backend database """ if self.is_sonarcloud(): - return "postgres" + return "postgresql" if self.version() < (9, 7, 0): return self.sys_info()["Statistics"]["database"]["name"] return self.sys_info()["Database"]["Database"] @@ -395,7 +395,7 @@ def reset_setting(self, key: str) -> bool: :param key: Setting key :return: Whether the reset was successful or not """ - return settings.reset_setting(self, key).ok + return settings.reset_setting(self, key) def set_setting(self, key: str, value: any) -> bool: """Sets a platform global setting @@ -415,7 +415,7 @@ def __urlstring(self, api: str, params: types.ApiParams, data: str = None) -> st if isinstance(v, datetime.date): good_params[k] = util.format_date(v) elif isinstance(v, (list, tuple, set)): - good_params[k] = ",".join(list(v)) + good_params[k] = ",".join([str(x) for x in v]) params_string = "&".join([f"{k}={requests.utils.quote(str(v))}" for k, v in good_params.items()]) if len(params_string) > 0: url += f"?{params_string}" @@ -442,7 +442,7 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t """ log.info("Exporting platform global settings") json_data = {} - for s in self.__settings(include_not_set=export_settings["EXPORT_DEFAULTS"]).values(): + for s in self.__settings(include_not_set=export_settings.get("EXPORT_DEFAULTS", False)).values(): if s.is_internal(): continue (categ, subcateg) = s.category() @@ -451,7 +451,7 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t continue if not s.is_global(): continue - util.update_json(json_data, categ, subcateg, s.to_json(export_settings["INLINE_LISTS"])) + util.update_json(json_data, categ, subcateg, s.to_json(export_settings.get("INLINE_LISTS", True))) hooks = {} for wb in self.webhooks().values(): diff --git a/sonar/settings.py b/sonar/settings.py index cd04dfe1e..75779a2f7 100644 --- a/sonar/settings.py +++ b/sonar/settings.py @@ -224,7 +224,7 @@ def reload(self, data: types.ApiPayload) -> None: def __hash__(self) -> int: """Returns object unique ID""" - return hash((self.key, self.component.key if self.component else "", self.endpoint.url)) + return hash((self.key, self.component.key if self.component else None, self.endpoint.url)) def __str__(self) -> str: if self.component is None: @@ -548,7 +548,7 @@ def decode(setting_key: str, setting_value: any) -> any: def reset_setting(endpoint: pf.Platform, setting_key: str, project_key: str = None) -> bool: """Resets a setting to its default""" log.info("Resetting setting '%s", setting_key) - return endpoint.post("settings/reset", params={"key": setting_key, "component": project_key}).ok + return endpoint.post("settings/reset", params={"keys": setting_key, "component": project_key}).ok def get_component_params(component: object, name: str = "component") -> types.ApiParamss: diff --git a/test/files/config.json b/test/files/config.json index f4721f61d..287e03431 100644 --- a/test/files/config.json +++ b/test/files/config.json @@ -12,7 +12,6 @@ "sonar-administrators": "admin" } }, - "tags": "", "visibility": "public" }, "APP_TEST": { @@ -92,7 +91,6 @@ "sonar-users": "user" } }, - "tags": "", "visibility": "private" }, "App_with_no_perms": { @@ -107,68 +105,56 @@ "admin": "admin, user" } }, - "tags": "", "visibility": "private" + }, + "MON": { + "branches": { + "main": { + "isMain": true, + "projects": { + "demo:jcl": "main", + "demo:target-awareness": "main", + "exclusions-2": "main" + } + } + }, + "name": "My monorepo", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin" + } + }, + "visibility": "public" } }, "globalSettings": { - "analysisScope": { - "sonar.coverage.exclusions": "", - "sonar.cpd.exclusions": "", - "sonar.exclusions": "", - "sonar.global.exclusions": "", - "sonar.global.test.exclusions": "", - "sonar.inclusions": "", - "sonar.issue.enforce.multicriteria": "", - "sonar.issue.ignore.allfile": "", - "sonar.issue.ignore.block": "", - "sonar.issue.ignore.multicriteria": "", - "sonar.test.exclusions": "", - "sonar.test.inclusions": "" - }, "authentication": { "sonar.auth.bitbucket.allowUsersToSignUp": true, - "sonar.auth.bitbucket.clientId.secured": "", - "sonar.auth.bitbucket.clientSecret.secured": "", "sonar.auth.bitbucket.enabled": false, - "sonar.auth.bitbucket.workspaces": "", "sonar.auth.github.allowUsersToSignUp": true, "sonar.auth.github.apiUrl": "https://api.github.com/", "sonar.auth.github.appId": 946173, - "sonar.auth.github.clientId.secured": "", - "sonar.auth.github.clientSecret.secured": "", "sonar.auth.github.enabled": false, "sonar.auth.github.groupsSync": false, "sonar.auth.github.organizations": "okorach-org", - "sonar.auth.github.privateKey.secured": "", "sonar.auth.github.webUrl": "https://github.com/", "sonar.auth.gitlab.allowUsersToSignUp": true, "sonar.auth.gitlab.allowedGroups": "gl-admins", - "sonar.auth.gitlab.applicationId.secured": "", "sonar.auth.gitlab.enabled": true, "sonar.auth.gitlab.groupsSync": true, - "sonar.auth.gitlab.secret.secured": "", "sonar.auth.gitlab.url": "https://gitlab.com/", "sonar.auth.saml.applicationId": "sonarqube", - "sonar.auth.saml.certificate.secured": "", "sonar.auth.saml.enabled": false, - "sonar.auth.saml.group.name": "", - "sonar.auth.saml.loginUrl": "", - "sonar.auth.saml.providerId": "", "sonar.auth.saml.providerName": "SAML", "sonar.auth.saml.signature.enabled": false, - "sonar.auth.saml.sp.certificate.secured": "", - "sonar.auth.saml.sp.privateKey.secured": "", - "sonar.auth.saml.user.email": "", - "sonar.auth.saml.user.login": "", - "sonar.auth.saml.user.name": "", "sonar.auth.token.max.allowed.lifetime": "No expiration", "sonar.forceAuthentication": true }, "devopsIntegration": { "ADO": { "type": "azure", - "url": "https://dev.azure.com/okorach" + "url": "https://dev.azure.com/olivierkorach" }, "GitHub okorach": { "appId": "946159", @@ -188,18 +174,10 @@ } }, "generalSettings": { - "email.from": "noreply@nowhere", - "email.fromName": "SonarQube", - "email.prefix": "[SONARQUBE]", - "email.smtp_host.secured": "", - "email.smtp_password.secured": "", - "email.smtp_port.secured": "__default__", - "email.smtp_secure_connection.secured": "", - "email.smtp_username.secured": "", "provisioning.github.project.visibility.enabled": true, "provisioning.gitlab.enabled": false, - "provisioning.gitlab.token.secured": "", - "sonar.allowPermissionManagementForProjectAdmins": true, + "sonar.ai.suggestions.enabled": "ENABLED_FOR_SOME_PROJECTS", + "sonar.allowPermissionManagementForProjectAdmins": false, "sonar.announcement.displayMessage": false, "sonar.announcement.message": "You are on LATEST", "sonar.builtInQualityProfiles.disableNotificationOnUpdate": false, @@ -226,33 +204,30 @@ "sonar.developerAggregatedInfo.disabled": false, "sonar.governance.report.project.branch.frequency": "Monthly", "sonar.governance.report.view.frequency": "Monthly", - "sonar.governance.report.view.recipients": "", - "sonar.issues.defaultAssigneeLogin": "", "sonar.kubernetes.activate": true, "sonar.lf.enableGravatar": false, "sonar.lf.gravatarServerUrl": "https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon", - "sonar.lf.logoUrl": "", - "sonar.lf.logoWidthPx": "", - "sonar.login.displayMessage": "", - "sonar.login.message": "", "sonar.plugins.downloadOnlyRequired": true, "sonar.portfolios.confidential.header": true, - "sonar.portfolios.recompute.hours": "", "sonar.projectCreation.mainBranchName": "main", "sonar.qualityProfiles.allowDisableInheritedRules": true, "sonar.qualitygate.ignoreSmallChanges": true, "sonar.scm.disabled": false, "sonar.technicalDebt.developmentCost": 30, "sonar.technicalDebt.ratingGrid": "0.05,0.1,0.2,0.5", - "sonar.validateWebhooks": true + "sonar.validateWebhooks": true, + "webhooks": { + "Jenkins": { + "url": "https://jenkins.olivierk.ngrok.io" + } + } }, "languages": { "abap": { "sonar.abap.file.suffixes": ".ab4, .abap, .asprog, .flow" }, "apex": { - "sonar.apex.file.suffixes": ".cls, .trigger", - "sonar.apex.pmd.reportPaths": "" + "sonar.apex.file.suffixes": ".cls, .trigger" }, "azureresourcemanager": { "sonar.azureresourcemanager.activate": true, @@ -261,37 +236,19 @@ }, "cfamily": { "sonar.c.file.suffixes": ".c, .h", - "sonar.cfamily.bullseye.reportPath": "", - "sonar.cfamily.cobertura.reportPaths": "", - "sonar.cfamily.cppunit.reportsPath": "", - "sonar.cfamily.gcov.reportsPath": "", - "sonar.cfamily.llvm-cov.reportPath": "", - "sonar.cfamily.valgrind.reportsPaths": "", - "sonar.cfamily.vscoveragexml.reportsPath": "", "sonar.cpp.file.suffixes": ".c++, .cc, .cpp, .cxx, .h++, .hh, .hpp, .hxx, .ipp", "sonar.objc.file.suffixes": ".m" }, "cloudformation": { "sonar.cloudformation.activate": true, - "sonar.cloudformation.cfn-lint.reportPaths": "", "sonar.cloudformation.file.identifier": "AWSTemplateFormatVersion" }, "cobol": { "sonar.cobol.adaprep.activation": false, - "sonar.cobol.aucobol.preprocessor.directives.default": "", "sonar.cobol.byteBasedColumnCount": false, - "sonar.cobol.compilationConstants": "", - "sonar.cobol.copy.directories": "", - "sonar.cobol.copy.exclusions": "", - "sonar.cobol.copy.suffixes": "", - "sonar.cobol.db2include.directories": "", "sonar.cobol.dialect": "ibm-enterprise-cobol", "sonar.cobol.exec.recoveryMode": true, - "sonar.cobol.file.suffixes": "", - "sonar.cobol.preprocessor.skipping.first.matching.characters": "", "sonar.cobol.sourceFormat": "fixed", - "sonar.cobol.sql.catalog.csv.path": "", - "sonar.cobol.sql.catalog.defaultSchema": "", "sonar.cobol.tab.width": 8, "sonar.cpd.cobol.ignoreLiteral": true }, @@ -300,37 +257,24 @@ "sonar.cs.analyzeRazorCode": true, "sonar.cs.file.suffixes": ".cs", "sonar.cs.ignoreHeaderComments": true, - "sonar.cs.roslyn.bugCategories": "", - "sonar.cs.roslyn.codeSmellCategories": "", - "sonar.cs.roslyn.ignoreIssues": false, - "sonar.cs.roslyn.vulnerabilityCategories": "" + "sonar.cs.roslyn.ignoreIssues": false }, "css": { - "sonar.css.file.suffixes": ".css, .less, .scss", - "sonar.css.stylelint.reportPaths": "" + "sonar.css.file.suffixes": ".css, .less, .scss" }, "dart": { - "sonar.dart.file.suffixes": ".dart", - "sonar.dart.lcov.reportPaths": "" + "sonar.dart.file.suffixes": ".dart" }, "docker": { "sonar.docker.activate": true, - "sonar.docker.file.patterns": "*.dockerfile, Dockerfile, *.Dockerfile", - "sonar.docker.hadolint.reportPaths": "" + "sonar.docker.file.patterns": "*.dockerfile, Dockerfile, *.Dockerfile" }, "flex": { - "sonar.flex.cobertura.reportPaths": "", "sonar.flex.file.suffixes": "as" }, "go": { - "sonar.go.coverage.reportPaths": "", "sonar.go.exclusions": "**/vendor/**", - "sonar.go.file.suffixes": ".go", - "sonar.go.golangci-lint.reportPaths": "", - "sonar.go.golint.reportPaths": "", - "sonar.go.gometalinter.reportPaths": "", - "sonar.go.govet.reportPaths": "", - "sonar.go.tests.reportPaths": "" + "sonar.go.file.suffixes": ".go" }, "html": { "sonar.html.file.suffixes": ".ascx, .aspx, .cmp, .cshtml, .erb, .html, .rhtml, .shtm, .shtml, .twig, .vbhtml, .xhtml" @@ -344,17 +288,13 @@ "sonar.java.file.suffixes": ".jav, .java", "sonar.java.ignoreUnnamedModuleForSplitPackage": false, "sonar.java.jvmframeworkconfig.activate": true, - "sonar.java.jvmframeworkconfig.file.patterns": "**/src/main/resources/**/application*.properties, **/src/main/resources/**/application*.yaml, **/src/main/resources/**/application*.yml", - "sonar.java.pmd.reportPaths": "", - "sonar.java.spotbugs.reportPaths": "" + "sonar.java.jvmframeworkconfig.file.patterns": "**/src/main/resources/**/application*.properties, **/src/main/resources/**/application*.yaml, **/src/main/resources/**/application*.yml" }, "javascript": { - "sonar.eslint.reportPaths": "", "sonar.javascript.environments": "amd, applescript, atomtest, browser, commonjs, couch, embertest, flow, greasemonkey, jasmine, jest, jquery, meteor, mocha, mongo, nashorn, node, phantomjs, prototypejs, protractor, qunit, rhino, serviceworker, shared-node-browser, shelljs, webextensions, worker, wsh, yui", "sonar.javascript.file.suffixes": ".cjs, .js, .jsx, .mjs, .vue", "sonar.javascript.globals": "Backbone, OenLayers, _, angular, casper, d3, dijit, dojo, dojox, goog, google, moment, sap", "sonar.javascript.ignoreHeaderComments": true, - "sonar.javascript.lcov.reportPaths": "", "sonar.javascript.maxFileSize": 1000 }, "jcl": { @@ -367,19 +307,12 @@ "sonar.jsp.file.suffixes": ".jsp, .jspf, .jspx" }, "kotlin": { - "sonar.androidLint.reportPaths": "", - "sonar.kotlin.detekt.reportPaths": "", - "sonar.kotlin.file.suffixes": ".kt", - "sonar.kotlin.ktlint.reportPaths": "" + "sonar.kotlin.file.suffixes": ".kt" }, "php": { - "sonar.php.coverage.reportPaths": "", "sonar.php.exclusions": "**/vendor/**", "sonar.php.file.suffixes": "inc, php, php3, php4, php5, phtml", - "sonar.php.frameworkDetection": true, - "sonar.php.phpstan.reportPaths": "", - "sonar.php.psalm.reportPaths": "", - "sonar.php.tests.reportPath": "" + "sonar.php.frameworkDetection": true }, "pli": { "sonar.pli.extralingualCharacters": "#@$", @@ -393,14 +326,8 @@ "sonar.plsql.ignoreHeaderComments": false }, "python": { - "sonar.python.bandit.reportPaths": "", "sonar.python.coverage.reportPaths": "coverage-reports/*coverage-*.xml", "sonar.python.file.suffixes": "py", - "sonar.python.flake8.reportPaths": "", - "sonar.python.mypy.reportPaths": "", - "sonar.python.pylint.reportPaths": "", - "sonar.python.ruff.reportPaths": "", - "sonar.python.version": "", "sonar.python.xunit.reportPath": "xunit-reports/xunit-result-*.xml", "sonar.python.xunit.skipDetails": false }, @@ -411,30 +338,20 @@ "ruby": { "sonar.ruby.coverage.reportPaths": "coverage/.resultset.json", "sonar.ruby.exclusions": "**/vendor/**", - "sonar.ruby.file.suffixes": ".rb", - "sonar.ruby.rubocop.reportPaths": "" + "sonar.ruby.file.suffixes": ".rb" }, "scala": { - "sonar.scala.coverage.reportPaths": "", - "sonar.scala.file.suffixes": ".scala", - "sonar.scala.scalastyle.reportPaths": "", - "sonar.scala.scapegoat.reportPaths": "" + "sonar.scala.file.suffixes": ".scala" }, "swift": { - "sonar.swift.coverage.reportPaths": "", - "sonar.swift.file.suffixes": ".swift", - "sonar.swift.swiftLint.reportPaths": "" + "sonar.swift.file.suffixes": ".swift" }, "terraform": { "sonar.terraform.activate": true, - "sonar.terraform.file.suffixes": ".tf", - "sonar.terraform.provider.aws.version": "", - "sonar.terraform.provider.azure.version": "", - "sonar.terraform.tflint.reportPaths": "" + "sonar.terraform.file.suffixes": ".tf" }, "text": { "sonar.text.activate": true, - "sonar.text.excluded.file.suffixes": "", "sonar.text.inclusions": "**/*, **/*.bash, **/*.conf, **/*.config, **/*.ksh, **/*.pem, **/*.properties, **/*.ps1, **/*.sh, **/*.zsh, .aws/config, .env", "sonar.text.inclusions.activate": true }, @@ -442,9 +359,7 @@ "sonar.tsql.file.suffixes": ".tsql" }, "typescript": { - "sonar.typescript.file.suffixes": ".cts, .mts, .ts, .tsx", - "sonar.typescript.tsconfigPaths": "", - "sonar.typescript.tslint.reportPaths": "" + "sonar.typescript.file.suffixes": ".cts, .mts, .ts, .tsx" }, "vb": { "sonar.vb.file.suffixes": ".BAS, .CLS, .CTL, .FRM, .bas, .cls, .ctl, .frm", @@ -454,10 +369,7 @@ "sonar.vbnet.analyzeGeneratedCode": false, "sonar.vbnet.file.suffixes": ".vb", "sonar.vbnet.ignoreHeaderComments": true, - "sonar.vbnet.roslyn.bugCategories": "", - "sonar.vbnet.roslyn.codeSmellCategories": "", - "sonar.vbnet.roslyn.ignoreIssues": false, - "sonar.vbnet.roslyn.vulnerabilityCategories": "" + "sonar.vbnet.roslyn.ignoreIssues": false }, "xml": { "sonar.xml.file.suffixes": ".xml, .xsd, .xsl" @@ -467,16 +379,12 @@ } }, "linters": { - "sonar.checkstyle.checker.tabWidth": "", "sonar.checkstyle.filters": "", "sonar.checkstyle.treewalkerfilters": "", "sonar.findbugs.allowuncompiledcode": false, "sonar.findbugs.analyzeTests": true, "sonar.findbugs.confidenceLevel": "medium", "sonar.findbugs.effort": "Default", - "sonar.findbugs.excludesFilters": "", - "sonar.findbugs.onlyAnalyze": "", - "sonar.findbugs.reportpaths": "", "sonar.findbugs.timeout": 600000 }, "permissionTemplates": { @@ -494,9 +402,12 @@ "description": "This permission template will be used as default when no other permission configuration is available", "permissions": { "groups": { - "project-admins": "admin, user", - "sonar-administrators": "admin", - "sonar-users": "codeviewer, issueadmin, securityhotspotadmin, user" + "developers": "codeviewer, user", + "project-admins": "admin, codeviewer, user", + "security-auditors": "codeviewer, issueadmin, securityhotspotadmin, user", + "sonar-administrators": "admin, codeviewer, user", + "sonar-users": "user", + "tech-leads": "codeviewer, issueadmin, user" } } }, @@ -520,7 +431,7 @@ }, "9. Bad Template with wildcard instead of regexp": { "description": "Bad template that uses wildcard project key pattern instead of regexp", - "pattern": "BANKING-INVEST-*" + "pattern": "BANKING-INVEST-.*" }, "Default template": { "description": "Default", @@ -534,23 +445,15 @@ }, "permissions": { "groups": { - "language-experts": "profileadmin, provisioning, scan", - "quality-managers": "gateadmin, scan", - "sonar-administrators": "admin, applicationcreator, gateadmin, portfoliocreator, profileadmin, provisioning", - "sonar-users": "provisioning, scan" + "ci-tools": "provisioning, scan", + "language-experts": "profileadmin", + "quality-managers": "gateadmin", + "sonar-administrators": "admin, applicationcreator, gateadmin, portfoliocreator, profileadmin, provisioning, scan", + "sonar-users": "applicationcreator, portfoliocreator" } }, - "sastConfig": { - "sonar.security.config.javasecurity": "", - "sonar.security.config.phpsecurity": "", - "sonar.security.config.pythonsecurity": "", - "sonar.security.config.roslyn.sonaranalyzer.security.cs": "" - }, - "tests": { - "sonar.coverage.jacoco.xmlReportPaths": "", - "sonar.junit.reportPaths": "" - }, "thirdParty": { + "sonar.ansible.activate": true, "sonar.dependencyCheck.htmlReportPath": "${WORKSPACE}/dependency-check-report.html", "sonar.dependencyCheck.jsonReportPath": "${WORKSPACE}/dependency-check-report.json", "sonar.dependencyCheck.securityHotspot": false, @@ -559,20 +462,19 @@ "sonar.dependencyCheck.severity.medium": 4.0, "sonar.dependencyCheck.skip": false, "sonar.dependencyCheck.summarize": false, - "sonar.dependencyCheck.useFilePath": false + "sonar.dependencyCheck.useFilePath": false, + "sonar.scanner.skipNodeProvisioning": false } }, "groups": { + "ci-tools": "Service accounts for CI tools", "developers": "Developers", - "gl-admins": "", - "gl-admins/gl-devs": "", "language-experts": "Language experts in charge of defining the company governance in terms of Quality Profiles (rulesets enforced in the company)", "project-admins": "Project administrators in charge of project configuration", "quality-managers": "Quality Managers in charge of defining company governance in terms of quality gates", "security-auditors": "Security Auditors in charge of reviewing security issues", "sonar-administrators": "SonarQube administrators", - "tech-leads": "Senior developers in charge of reviewing issues", - "z comma , group": "" + "tech-leads": "Senior developers in charge of reviewing issues" }, "platform": { "edition": "enterprise", @@ -584,7 +486,7 @@ }, "serverId": "243B8A4D-AY5SFSbmgIK8PCmM81th", "url": "https://latest.olivierk.ngrok.io", - "version": "10.7.0" + "version": "10.8.0" }, "portfolios": { "All": { @@ -867,6 +769,8 @@ }, "projects": { "BANKING-AFRICA-OPS": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -886,6 +790,8 @@ "visibility": "public" }, "BANKING-ASIA-OPS": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -905,6 +811,8 @@ "visibility": "public" }, "BANKING-INVESTMENT-EQUITY": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -924,6 +832,8 @@ "visibility": "public" }, "BANKING-INVESTMENT-MERGER": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -943,6 +853,7 @@ "visibility": "public" }, "BANKING-PORTAL": { + "aiCodeFix": false, "branches": { "comma,branch": { "keepWhenInactive": true @@ -954,6 +865,7 @@ "keepWhenInactive": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -972,6 +884,8 @@ "visibility": "public" }, "BANKING-PRIVATE-ASSETS": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -990,6 +904,8 @@ "visibility": "public" }, "BANKING-PRIVATE-WEALTH": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1008,6 +924,8 @@ "visibility": "public" }, "BANKING-RETAIL-ATM": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1027,6 +945,8 @@ "visibility": "public" }, "BANKING-RETAIL-CLERK": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1046,6 +966,8 @@ "visibility": "public" }, "BANKING-TRADING-EURO": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1065,6 +987,8 @@ "visibility": "public" }, "BANKING-TRADING-JAPAN": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1084,6 +1008,8 @@ "visibility": "public" }, "BANKING-TRADING-NASDAQ": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1103,6 +1029,8 @@ "visibility": "public" }, "INSURANCE-HOME": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1122,6 +1050,8 @@ "visibility": "public" }, "INSURANCE-LIFE": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1141,6 +1071,8 @@ "visibility": "public" }, "INSURANCE-PET": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1160,6 +1092,8 @@ "visibility": "public" }, "Project-with-no-perms": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Project with no perms", "permissions": { "groups": { @@ -1171,6 +1105,8 @@ "visibility": "public" }, "RETAIL-ATM": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Retail - ATM", "permissions": { "groups": { @@ -1182,6 +1118,8 @@ "visibility": "public" }, "RETAIL-WEB": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1201,11 +1139,13 @@ "visibility": "public" }, "TEST": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach-org", "repository": "okorach-org/demo-github-azdo", "summaryCommentEnabled": true }, + "containsAiCode": false, "links": [ { "name": "google", @@ -1362,9 +1302,6 @@ } }, "qualityGate": "Sonar way", - "qualityProfiles": { - "py": "Olivier Way" - }, "visibility": "public", "webhooks": { "Jenkins": { @@ -1373,6 +1310,7 @@ } }, "TESTSYNC": { + "aiCodeFix": false, "branches": { "main": { "keepWhenInactive": true @@ -1381,6 +1319,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "TESTSYNC", "permissions": { "groups": { @@ -1391,11 +1330,13 @@ "visibility": "public" }, "TESTSYNC2": { + "aiCodeFix": false, "branches": { "main-branch": { "isMain": true } }, + "containsAiCode": false, "name": "TESTSYNC2", "permissions": { "groups": { @@ -1405,7 +1346,26 @@ }, "visibility": "public" }, + "ai-code-fix": { + "aiCodeFix": true, + "branches": { + "main": { + "isMain": true + } + }, + "containsAiCode": true, + "name": "AI CodeFix examples", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + } + }, + "visibility": "public" + }, "checkstyle-issues": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1414,6 +1374,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1429,12 +1390,11 @@ "sonar-users": "issueadmin, securityhotspotadmin" } }, - "qualityProfiles": { - "java": "Java and Checkstyle" - }, "visibility": "public" }, "demo-rules": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Demo rules", "permissions": { "groups": { @@ -1445,7 +1405,27 @@ }, "visibility": "public" }, + "demo:ado-cli": { + "aiCodeFix": false, + "binding": { + "key": "ADO", + "repository": "demo-ado-cli", + "slug": "Demos" + }, + "containsAiCode": false, + "name": "demo-ado-cli", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + } + }, + "visibility": "public" + }, "demo:autoconfig": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Demo Autoconfig", "permissions": { "groups": { @@ -1457,11 +1437,13 @@ "visibility": "public" }, "demo:autoconfig:carbon": { + "aiCodeFix": false, "branches": { "develop": { "isMain": true } }, + "containsAiCode": false, "name": "Demo Autoconfig Carbon", "permissions": { "groups": { @@ -1473,11 +1455,13 @@ "visibility": "public" }, "demo:github-actions-cli": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach-org", "repository": "okorach-org/demo-actions-cli", "summaryCommentEnabled": true }, + "containsAiCode": false, "name": "GitHub / Actions / CLI", "permissions": { "groups": { @@ -1488,7 +1472,30 @@ }, "visibility": "public" }, + "demo:github-actions-maven": { + "aiCodeFix": false, + "binding": { + "key": "GitHub okorach", + "repository": "okorach/demo-actions-maven", + "summaryCommentEnabled": true + }, + "containsAiCode": false, + "name": "GitHub / Actions / Maven", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + }, + "users": { + "admin": "scan" + } + }, + "qualityGate": "Siemens Healthineers QG", + "visibility": "public" + }, "demo:github-actions-mono-cli": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach", "monorepo": true, @@ -1500,6 +1507,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "GitHub / Actions / monorepo CLI", "permissions": { "groups": { @@ -1511,6 +1519,7 @@ "visibility": "public" }, "demo:github-actions-mono-dotnet": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach", "monorepo": true, @@ -1522,6 +1531,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "GitHub / Actions / monorepo .Net Core", "permissions": { "groups": { @@ -1533,6 +1543,7 @@ "visibility": "public" }, "demo:github-actions-mono-gradle": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach", "monorepo": true, @@ -1544,6 +1555,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "GitHub / Actions / monorepo Gradle", "permissions": { "groups": { @@ -1556,6 +1568,7 @@ "visibility": "public" }, "demo:github-actions-mono-maven": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach", "monorepo": true, @@ -1570,6 +1583,7 @@ "keepWhenInactive": true } }, + "containsAiCode": false, "name": "GitHub / Actions / monorepo Maven", "permissions": { "groups": { @@ -1582,6 +1596,8 @@ "visibility": "public" }, "demo:gitlab-ci-maven": { + "aiCodeFix": false, + "containsAiCode": false, "name": "GitLab-CI / Maven", "permissions": { "groups": { @@ -1593,6 +1609,8 @@ "visibility": "public" }, "demo:gitlab:gradle": { + "aiCodeFix": false, + "containsAiCode": false, "name": "GitLab / Gradle", "permissions": { "groups": { @@ -1604,6 +1622,7 @@ "visibility": "public" }, "demo:gitlab:scanner-cli": { + "aiCodeFix": false, "branches": { "main": { "isMain": true @@ -1612,6 +1631,7 @@ "keepWhenInactive": true } }, + "containsAiCode": false, "name": "GitLab / Scanner CLI", "permissions": { "groups": { @@ -1623,6 +1643,8 @@ "visibility": "public" }, "demo:java-security": { + "aiCodeFix": false, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1641,6 +1663,8 @@ "visibility": "public" }, "demo:jcl": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Demo JCL", "permissions": { "groups": { @@ -1649,13 +1673,12 @@ "sonar-users": "issueadmin, securityhotspotadmin" } }, - "qualityProfiles": { - "jcl": "Full Way" - }, "visibility": "public" }, "demo:secrets": { - "name": "Demo custom secrets", + "aiCodeFix": false, + "containsAiCode": false, + "name": "Demo Secrets Detection", "permissions": { "groups": { "project-admins": "admin", @@ -1666,6 +1689,8 @@ "visibility": "public" }, "demo:target-awareness": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Demo: Target awareness", "permissions": { "groups": { @@ -1677,6 +1702,8 @@ "visibility": "public" }, "dotnet-with-cli": { + "aiCodeFix": false, + "containsAiCode": false, "name": "dotnet-with-cli", "permissions": { "groups": { @@ -1688,7 +1715,8 @@ "visibility": "public" }, "dvpa": { - "aiCodeAssurance": true, + "aiCodeFix": false, + "containsAiCode": true, "name": "dvpa", "newCodePeriod": "NUMBER_OF_DAYS = 30", "permissions": { @@ -1702,7 +1730,22 @@ "sonar.cfamily.generateComputedConfig": false, "visibility": "private" }, + "exclusions-2": { + "aiCodeFix": false, + "containsAiCode": false, + "name": "example-2", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + } + }, + "visibility": "public" + }, "foobar": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Super long long project name that makes me crazy", "permissions": { "groups": { @@ -1713,6 +1756,8 @@ "visibility": "public" }, "gradle-with-cli": { + "aiCodeFix": false, + "containsAiCode": false, "name": "gradle-with-cli", "permissions": { "groups": { @@ -1723,7 +1768,39 @@ }, "visibility": "public" }, + "juice-shop": { + "aiCodeFix": false, + "containsAiCode": false, + "links": [ + { + "name": "scm", + "type": "scm", + "url": "https://github.com/juice-shop/juice-shop.git" + }, + { + "name": "issue", + "type": "issue", + "url": "https://github.com/juice-shop/juice-shop/issues" + }, + { + "name": "homepage", + "type": "homepage", + "url": "https://owasp-juice.shop" + } + ], + "name": "juice-shop", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + } + }, + "visibility": "public" + }, "maven-with-cli": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Maven project analyzed with Scanner CLI", "permissions": { "groups": { @@ -1735,6 +1812,8 @@ "visibility": "public" }, "no-scm": { + "aiCodeFix": false, + "containsAiCode": false, "name": "No SCM", "permissions": { "groups": { @@ -1747,6 +1826,7 @@ "visibility": "public" }, "okorach-org_pr-demo_3a1857ec-cebc-49f2-96ac-9bbc99111469": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach-org", "repository": "okorach-org/pr-demo", @@ -1757,6 +1837,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "pr-demo", "permissions": { "groups": { @@ -1768,6 +1849,8 @@ "visibility": "public" }, "okorach_audio-video-tools": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Audio Video Tools", "permissions": { "groups": { @@ -1780,6 +1863,7 @@ "visibility": "public" }, "okorach_demo-gitlabci-cli_e81d5112-e681-44b2-aee4-62b56c8ac5cb": { + "aiCodeFix": false, "binding": { "key": "gitlab.com", "repository": "30584574", @@ -1790,6 +1874,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "Demo GitLab-CI CLI", "permissions": { "users": { @@ -1799,11 +1884,13 @@ "visibility": "public" }, "okorach_demo-gitlabci-maven": { + "aiCodeFix": false, "binding": { "key": "gitlab.com", "repository": "30599779", "summaryCommentEnabled": true }, + "containsAiCode": false, "name": "Demo Gitlab Maven", "permissions": { "users": { @@ -1814,6 +1901,7 @@ "visibility": "public" }, "okorach_sonar-tools": { + "aiCodeFix": false, "branches": { "comma,branch": { "keepWhenInactive": true @@ -1825,16 +1913,18 @@ "isMain": true } }, + "containsAiCode": false, "name": "Sonar Tools", "permissions": { "groups": { - "sonar-administrators": "admin", - "sonar-users": "issueadmin, securityhotspotadmin" + "developers": "codeviewer, user", + "project-admins": "admin, codeviewer, user", + "security-auditors": "codeviewer, issueadmin, securityhotspotadmin, user", + "sonar-administrators": "admin, codeviewer, user", + "sonar-users": "user", + "tech-leads": "codeviewer, issueadmin, user" } }, - "qualityProfiles": { - "py": "Olivier Way" - }, "sonar.cfamily.ignoreHeaderComments": false, "sonar.issue.ignore.multicriteria": [ { @@ -1847,9 +1937,10 @@ } ], "tags": "asia, favorites, insurance", - "visibility": "public" + "visibility": "private" }, "okorach_sonar-tools_a9480a73-8778-4aea-8796-240cc13686fb": { + "aiCodeFix": false, "binding": { "key": "GitHub okorach", "repository": "okorach/sonar-tools", @@ -1860,6 +1951,7 @@ "isMain": true } }, + "containsAiCode": false, "name": "sonar-tools", "permissions": { "groups": { @@ -1873,6 +1965,8 @@ "visibility": "public" }, "project-without-analyses": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Project without analyses", "permissions": { "groups": { @@ -1884,6 +1978,7 @@ "visibility": "public" }, "project1": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1892,6 +1987,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1911,6 +2007,7 @@ "visibility": "public" }, "project2": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1919,6 +2016,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1937,6 +2035,7 @@ "visibility": "public" }, "project3": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1945,6 +2044,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1963,6 +2063,7 @@ "visibility": "public" }, "project4": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1971,6 +2072,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -1987,6 +2089,7 @@ "visibility": "public" }, "proyecto5": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -1995,6 +2098,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -2013,6 +2117,8 @@ "visibility": "public" }, "pytorch": { + "aiCodeFix": false, + "containsAiCode": false, "name": "pytorch", "permissions": { "groups": { @@ -2024,6 +2130,8 @@ "visibility": "private" }, "rem7annum": { + "aiCodeFix": false, + "containsAiCode": false, "name": "rem", "permissions": { "groups": { @@ -2035,6 +2143,8 @@ "visibility": "public" }, "ren6annum3": { + "aiCodeFix": false, + "containsAiCode": false, "name": "wdfefr", "permissions": { "groups": { @@ -2046,6 +2156,8 @@ "visibility": "public" }, "secrets-leak": { + "aiCodeFix": false, + "containsAiCode": false, "name": "secrets-leak", "permissions": { "groups": { @@ -2056,6 +2168,8 @@ "visibility": "public" }, "testsonar-tools": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Sonar Tools Test", "permissions": { "groups": { @@ -2067,6 +2181,8 @@ "visibility": "public" }, "training:complexity": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Training: Cyclomatic vs Cognitive complexity", "permissions": { "groups": { @@ -2077,7 +2193,27 @@ }, "visibility": "public" }, + "training:coverage": { + "aiCodeFix": false, + "branches": { + "main": { + "isMain": true + } + }, + "containsAiCode": false, + "name": "Training: Coverage", + "permissions": { + "groups": { + "project-admins": "admin", + "sonar-administrators": "admin", + "sonar-users": "issueadmin, securityhotspotadmin" + } + }, + "visibility": "public" + }, "training:external-issues": { + "aiCodeFix": false, + "containsAiCode": false, "name": "Training: External issues import", "permissions": { "groups": { @@ -2088,6 +2224,7 @@ "visibility": "public" }, "training:security": { + "aiCodeFix": false, "branches": { "develop": { "keepWhenInactive": true @@ -2096,6 +2233,7 @@ "isMain": true } }, + "containsAiCode": false, "links": [ { "name": "homepage", @@ -2114,6 +2252,8 @@ "visibility": "public" }, "vscode": { + "aiCodeFix": false, + "containsAiCode": false, "name": "vscode", "permissions": { "groups": { @@ -2155,6 +2295,17 @@ "new_violations >= 0" ] }, + "Siemens Healthineers QG": { + "conditions": [ + "new_coverage <= 80", + "new_duplicated_lines_density >= 3", + "new_security_hotspots_reviewed <= 100", + "new_software_quality_blocker_issues >= 0", + "new_software_quality_high_issues >= 0", + "new_software_quality_low_issues >= 0", + "new_software_quality_medium_issues >= 0" + ] + }, "Sonar way": { "isBuiltIn": true }, @@ -2168,6 +2319,9 @@ "new_security_rating >= A" ] }, + "Sonar way for AI Code": { + "isBuiltIn": true + }, "Sonar way w/o coverage": { "conditions": [ "new_coverage <= 0", @@ -2192,6 +2346,12 @@ "isDefault": true } }, + "ansible": { + "Sonar way": { + "isBuiltIn": true, + "isDefault": true + } + }, "apex": { "Sonar way": { "isBuiltIn": true, @@ -2199,316 +2359,12 @@ } }, "azureresourcemanager": { - "QP with no perms": { - "rules": { - "azureresourcemanager:S1135": "INFO", - "azureresourcemanager:S117": { - "params": { - "format": "^[a-z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "azureresourcemanager:S1192": { - "params": { - "minimal_literal_length": "5", - "threshold": "5" - }, - "severity": "CRITICAL" - }, - "azureresourcemanager:S1481": "MINOR", - "azureresourcemanager:S4423": "CRITICAL", - "azureresourcemanager:S4507": "MINOR", - "azureresourcemanager:S5332": "CRITICAL", - "azureresourcemanager:S6321": "MINOR", - "azureresourcemanager:S6329": "BLOCKER", - "azureresourcemanager:S6364": { - "params": { - "backup_retention_duration": "30" - }, - "severity": "MAJOR" - }, - "azureresourcemanager:S6378": "MAJOR", - "azureresourcemanager:S6379": "MAJOR", - "azureresourcemanager:S6380": "MAJOR", - "azureresourcemanager:S6381": "MAJOR", - "azureresourcemanager:S6382": "MAJOR", - "azureresourcemanager:S6383": "MAJOR", - "azureresourcemanager:S6385": "MAJOR", - "azureresourcemanager:S6387": "MAJOR", - "azureresourcemanager:S6388": "MAJOR", - "azureresourcemanager:S6413": { - "params": { - "log_retention_duration": "14" - }, - "severity": "MAJOR" - }, - "azureresourcemanager:S6437": "BLOCKER", - "azureresourcemanager:S6648": "CRITICAL", - "azureresourcemanager:S6656": "CRITICAL", - "azureresourcemanager:S6874": "MAJOR", - "azureresourcemanager:S6949": "MAJOR", - "azureresourcemanager:S6952": "MAJOR", - "azureresourcemanager:S6953": "MAJOR", - "azureresourcemanager:S6954": "MAJOR", - "azureresourcemanager:S6955": "MINOR", - "azureresourcemanager:S6956": "MINOR", - "azureresourcemanager:S6975": "MAJOR" - } - }, "Sonar way": { "isBuiltIn": true, "isDefault": true } }, "c": { - "My Way": { - "rules": { - "c:S1036": "BLOCKER", - "c:S1065": "MAJOR", - "c:S1066": "MAJOR", - "c:S107": { - "params": { - "max": "7" - }, - "severity": "MAJOR" - }, - "c:S1079": "CRITICAL", - "c:S108": "MAJOR", - "c:S1081": "CRITICAL", - "c:S1103": "MINOR", - "c:S1110": "MAJOR", - "c:S1116": "MINOR", - "c:S1117": "MAJOR", - "c:S1121": "MAJOR", - "c:S1123": "MAJOR", - "c:S1133": "INFO", - "c:S1134": "MAJOR", - "c:S1135": "INFO", - "c:S1144": "MAJOR", - "c:S1172": "MAJOR", - "c:S1186": "CRITICAL", - "c:S1198": "MAJOR", - "c:S1199": "MINOR", - "c:S1219": "BLOCKER", - "c:S125": "MAJOR", - "c:S1264": "MINOR", - "c:S128": "BLOCKER", - "c:S1301": "MINOR", - "c:S1313": "MINOR", - "c:S134": { - "params": { - "max": "3" - }, - "severity": "CRITICAL" - }, - "c:S1479": { - "params": { - "maximum": "30" - }, - "severity": "MAJOR" - }, - "c:S1481": "MINOR", - "c:S1656": "MAJOR", - "c:S1659": "MINOR", - "c:S1751": "MAJOR", - "c:S1760": "BLOCKER", - "c:S1761": "CRITICAL", - "c:S1762": "MINOR", - "c:S1763": "MAJOR", - "c:S1764": "MAJOR", - "c:S1767": "CRITICAL", - "c:S1768": "CRITICAL", - "c:S1820": { - "params": { - "maximumFieldThreshold": "20" - }, - "severity": "MAJOR" - }, - "c:S1831": "CRITICAL", - "c:S1836": "CRITICAL", - "c:S1854": "MAJOR", - "c:S1862": "MAJOR", - "c:S1871": "MAJOR", - "c:S1874": "MINOR", - "c:S1905": "MINOR", - "c:S1909": "BLOCKER", - "c:S1911": "MAJOR", - "c:S1912": { - "params": { - "nonReentrantFunctionList": "::asctime,::crypt,::ctermid,::ctime,::fgetgrent,::fgetpwent,::fgetspent,::getgrent,::getgrgid,::getgrnam,::gethostbyaddr,::gethostbyname,::gethostbyname2,::gethostent,::getlogin,::getnetbyaddr,::getnetbyname,::getnetent,::getnetgrent,::getprotobyname,::getprotobynumber,::getprotoent,::getpwent,::getpwnam,::getpwuid,::getrpcbyname,::getrpcbynumber,::getrpcent,::getservbyname,::getservbyport,::getservent,::getspent,::getspnam,::gmtime,::localtime,::sgetspent,::strtok,::ttyname" - }, - "severity": "BLOCKER" - }, - "c:S1913": "MAJOR", - "c:S1916": "MINOR", - "c:S2068": "BLOCKER", - "c:S2095": "BLOCKER", - "c:S2123": "MAJOR", - "c:S2190": "BLOCKER", - "c:S2193": "MINOR", - "c:S2216": "MAJOR", - "c:S2234": "MAJOR", - "c:S2245": "CRITICAL", - "c:S2259": "MAJOR", - "c:S2275": "BLOCKER", - "c:S2323": "CRITICAL", - "c:S2479": "CRITICAL", - "c:S2583": "MAJOR", - "c:S2612": "MAJOR", - "c:S2637": "MINOR", - "c:S2665": "MINOR", - "c:S2668": "MAJOR", - "c:S2681": "MAJOR", - "c:S2753": "MAJOR", - "c:S2754": "MINOR", - "c:S2755": "BLOCKER", - "c:S2757": "MAJOR", - "c:S2761": "MAJOR", - "c:S3135": "MAJOR", - "c:S3231": "MINOR", - "c:S3358": "MAJOR", - "c:S3457": "MAJOR", - "c:S3458": "MINOR", - "c:S3491": "BLOCKER", - "c:S3518": "CRITICAL", - "c:S3519": "BLOCKER", - "c:S3520": "BLOCKER", - "c:S3529": "BLOCKER", - "c:S3562": "MAJOR", - "c:S3584": "BLOCKER", - "c:S3588": "BLOCKER", - "c:S3590": "BLOCKER", - "c:S3646": "MINOR", - "c:S3687": "MAJOR", - "c:S3689": "MAJOR", - "c:S3728": "MINOR", - "c:S3729": "BLOCKER", - "c:S3730": "MINOR", - "c:S3744": "MINOR", - "c:S3776": { - "params": { - "maximumFunctionCognitiveComplexityThreshold": "25" - }, - "severity": "CRITICAL" - }, - "c:S3805": "MAJOR", - "c:S3806": "MAJOR", - "c:S3807": "CRITICAL", - "c:S3923": "MAJOR", - "c:S3935": "MAJOR", - "c:S3936": "BLOCKER", - "c:S3949": "MAJOR", - "c:S3972": "CRITICAL", - "c:S3973": "CRITICAL", - "c:S4143": "MAJOR", - "c:S4263": "MAJOR", - "c:S4423": "CRITICAL", - "c:S4426": "CRITICAL", - "c:S4524": "CRITICAL", - "c:S4790": "CRITICAL", - "c:S4830": "CRITICAL", - "c:S5000": "BLOCKER", - "c:S5042": "CRITICAL", - "c:S5259": "CRITICAL", - "c:S5261": "MAJOR", - "c:S5262": "MAJOR", - "c:S5263": "CRITICAL", - "c:S5266": "MAJOR", - "c:S5267": "BLOCKER", - "c:S5270": "MAJOR", - "c:S5271": "MINOR", - "c:S5273": "MAJOR", - "c:S5276": "MAJOR", - "c:S5278": "MAJOR", - "c:S5279": "MAJOR", - "c:S5280": "CRITICAL", - "c:S5281": "CRITICAL", - "c:S5283": "CRITICAL", - "c:S5293": "MINOR", - "c:S5297": "MAJOR", - "c:S5308": "CRITICAL", - "c:S5314": "CRITICAL", - "c:S5332": "CRITICAL", - "c:S5350": "MINOR", - "c:S5381": "MINOR", - "c:S5443": "CRITICAL", - "c:S5485": "CRITICAL", - "c:S5486": "BLOCKER", - "c:S5487": "BLOCKER", - "c:S5488": "CRITICAL", - "c:S5489": "BLOCKER", - "c:S5491": "MAJOR", - "c:S5494": "MAJOR", - "c:S5501": "MAJOR", - "c:S5527": "CRITICAL", - "c:S5542": "CRITICAL", - "c:S5547": "CRITICAL", - "c:S5570": "CRITICAL", - "c:S5658": "CRITICAL", - "c:S5782": "BLOCKER", - "c:S5798": "BLOCKER", - "c:S5801": "MAJOR", - "c:S5802": "CRITICAL", - "c:S5813": "MAJOR", - "c:S5814": "MAJOR", - "c:S5815": "MAJOR", - "c:S5816": "MAJOR", - "c:S5824": "CRITICAL", - "c:S5825": "MINOR", - "c:S5832": "MAJOR", - "c:S5847": "CRITICAL", - "c:S5849": "MAJOR", - "c:S5955": "MINOR", - "c:S5978": "MINOR", - "c:S5982": "CRITICAL", - "c:S6069": "CRITICAL", - "c:S6200": "CRITICAL", - "c:S6655": "BLOCKER", - "c:S6991": "BLOCKER", - "c:S6996": "MAJOR", - "c:S796": "MAJOR", - "c:S797": "BLOCKER", - "c:S798": "BLOCKER", - "c:S814": "MAJOR", - "c:S817": "MAJOR", - "c:S818": "MINOR", - "c:S819": "CRITICAL", - "c:S820": "CRITICAL", - "c:S824": "MAJOR", - "c:S833": "MAJOR", - "c:S836": "MAJOR", - "c:S841": "MAJOR", - "c:S859": "CRITICAL", - "c:S860": "MAJOR", - "c:S872": "MAJOR", - "c:S876": "MAJOR", - "c:S878": "MAJOR", - "c:S886": "MINOR", - "c:S897": "MAJOR", - "c:S905": "MAJOR", - "c:S912": "BLOCKER", - "c:S916": "BLOCKER", - "c:S923": "CRITICAL", - "c:S924": { - "params": { - "maxNumberOfTerminationStatements": "1" - }, - "severity": "MAJOR" - }, - "c:S935": "CRITICAL", - "c:S936": "CRITICAL", - "c:S946": "BLOCKER", - "c:S954": "MAJOR", - "c:S955": "MAJOR", - "c:S956": "MAJOR", - "c:S959": "CRITICAL", - "c:S961": "BLOCKER", - "c:S969": "BLOCKER", - "c:S977": "MAJOR", - "c:S995": "MINOR", - "c:S999": "BLOCKER" - } - }, "Sonar way": { "isBuiltIn": true, "isDefault": true @@ -2538,20 +2394,11 @@ "cs": { "Corp Way": { "children": { - "Team XXXX": { + "Team A": { "addedRules": { - "csharpsquid:S3884": "BLOCKER", - "csharpsquid:S4212": "MAJOR", - "csharpsquid:S4347": "CRITICAL", - "csharpsquid:S5344": "CRITICAL", - "csharpsquid:S6377": "MAJOR", - "csharpsquid:S6781": "BLOCKER", - "csharpsquid:S6932": "MAJOR", - "csharpsquid:S6967": "MAJOR", - "roslyn.sonaranalyzer.security.cs:S5167": "MINOR" - }, - "permissions": { - "users": "michal" + "csharpsquid:S126": "CRITICAL", + "csharpsquid:S3447": "CRITICAL", + "csharpsquid:S3962": "MINOR" } } }, @@ -2577,7 +2424,6 @@ "csharpsquid:S108": "MAJOR", "csharpsquid:S110": { "params": { - "filteredClasses": "", "max": "5" }, "severity": "MAJOR" @@ -2967,14 +2813,6 @@ }, "css": { "Sonar way": { - "children": { - "Acme corp way": { - "addedRules": { - "css:S4664": "CRITICAL", - "css:S5362": "CRITICAL" - } - } - }, "isBuiltIn": true, "isDefault": true } @@ -3022,763 +2860,6 @@ "FindBugs Security Minimal": { "isBuiltIn": true }, - "Java and Checkstyle": { - "rules": { - "checkstyle:com.puppycrawl.tools.checkstyle.checks.NoCodeInFileCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.OuterTypeFilenameCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.UpperEllCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.annotation.PackageAnnotationCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidDoubleBraceInitializationCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidInlineConditionalsCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidNoArgumentSuperConstructorCallCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.ConstructorsDeclarationGroupingCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.CovariantEqualsCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.EmptyStatementCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.EqualsHashCodeCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MissingCtorCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MissingSwitchDefaultCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MultipleVariableDeclarationsCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoArrayTrailingCommaCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoCloneCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoEnumTrailingCommaCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoFinalizerCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.OverloadMethodsDeclarationOrderCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.ParameterAssignmentCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanExpressionCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanReturnCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.StringLiteralEqualityCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SuperCloneCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SuperFinalizeCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.UnnecessarySemicolonInEnumerationCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.UnusedLocalVariableCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.InnerTypeLastCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocPositionCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocPackageCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.whitespace.GenericWhitespaceCheck": "MINOR", - "checkstyle:com.puppycrawl.tools.checkstyle.checks.whitespace.NoWhitespaceBeforeCaseDefaultColonCheck": "MINOR", - "java:Don_t_be_rude": { - "params": { - "message": "", - "regularExpression": "(fuck|shit|merde)" - }, - "severity": "MAJOR", - "templateKey": "java:S124" - }, - "java:S100": { - "params": { - "format": "^[a-z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S101": { - "params": { - "format": "^[A-Z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S106": "MAJOR", - "java:S1065": "MAJOR", - "java:S1066": "MAJOR", - "java:S1068": { - "params": { - "ignoreAnnotations": "" - }, - "severity": "MAJOR" - }, - "java:S107": { - "params": { - "constructorMax": "7", - "max": "7" - }, - "severity": "MAJOR" - }, - "java:S1075": "MINOR", - "java:S108": "MAJOR", - "java:S110": { - "params": { - "filteredClasses": "", - "max": "5" - }, - "severity": "MAJOR" - }, - "java:S1104": "MINOR", - "java:S1110": "MAJOR", - "java:S1111": "MAJOR", - "java:S1113": "CRITICAL", - "java:S1116": "MINOR", - "java:S1117": "MAJOR", - "java:S1118": "MAJOR", - "java:S1119": "MAJOR", - "java:S112": "MAJOR", - "java:S1121": "MAJOR", - "java:S1123": "MAJOR", - "java:S1124": "MINOR", - "java:S1125": "MINOR", - "java:S1126": "MINOR", - "java:S1128": "MINOR", - "java:S1130": "MINOR", - "java:S1133": "INFO", - "java:S1134": "MAJOR", - "java:S1135": "INFO", - "java:S114": { - "params": { - "format": "^[A-Z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S1141": "MAJOR", - "java:S1143": "CRITICAL", - "java:S1144": "MAJOR", - "java:S1149": "MAJOR", - "java:S115": { - "description": "En Francais: Les constantes doivent suivre des conventions de nommage", - "params": { - "format": "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$" - }, - "severity": "CRITICAL" - }, - "java:S1150": "MAJOR", - "java:S1153": "MINOR", - "java:S1155": "MINOR", - "java:S1157": "MINOR", - "java:S1158": "MINOR", - "java:S116": { - "params": { - "format": "^[a-z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S1161": "MAJOR", - "java:S1163": "CRITICAL", - "java:S1165": "MINOR", - "java:S1168": "MAJOR", - "java:S117": { - "params": { - "format": "^[a-z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S1170": "MINOR", - "java:S1171": "MAJOR", - "java:S1172": "MAJOR", - "java:S1174": "CRITICAL", - "java:S1175": "CRITICAL", - "java:S1181": "MAJOR", - "java:S1182": "MINOR", - "java:S1185": "MINOR", - "java:S1186": "CRITICAL", - "java:S119": { - "params": { - "format": "^[A-Z][0-9]?$" - }, - "severity": "MINOR" - }, - "java:S1190": "BLOCKER", - "java:S1191": { - "params": { - "Exclude": "" - }, - "severity": "MAJOR" - }, - "java:S1192": { - "params": { - "threshold": "3" - }, - "severity": "CRITICAL" - }, - "java:S1193": "MAJOR", - "java:S1195": "MINOR", - "java:S1197": "MINOR", - "java:S1199": "MINOR", - "java:S120": { - "params": { - "format": "^[a-z_]+(\\.[a-z_][a-z0-9_]*)*$" - }, - "severity": "MINOR" - }, - "java:S1201": "MAJOR", - "java:S1206": "MINOR", - "java:S1210": "MINOR", - "java:S1214": "CRITICAL", - "java:S1215": "CRITICAL", - "java:S1217": "MAJOR", - "java:S1219": "BLOCKER", - "java:S1220": "MINOR", - "java:S1221": "MAJOR", - "java:S1223": "MAJOR", - "java:S1226": "MINOR", - "java:S125": "MAJOR", - "java:S1264": "MINOR", - "java:S127": "MAJOR", - "java:S128": "BLOCKER", - "java:S1301": "MINOR", - "java:S131": "CRITICAL", - "java:S1313": "MINOR", - "java:S1317": "MAJOR", - "java:S1319": "MINOR", - "java:S135": "MINOR", - "java:S1444": "MINOR", - "java:S1450": "MINOR", - "java:S1451": { - "description": "Avec la meme version en francais", - "params": { - "headerFormat": "", - "isRegularExpression": "false" - }, - "severity": "BLOCKER" - }, - "java:S1452": "CRITICAL", - "java:S1479": { - "params": { - "maximum": "30" - }, - "severity": "MAJOR" - }, - "java:S1481": "MINOR", - "java:S1488": "MINOR", - "java:S1596": "MINOR", - "java:S1598": "CRITICAL", - "java:S1602": "MINOR", - "java:S1604": "MAJOR", - "java:S1607": "MAJOR", - "java:S1611": "MINOR", - "java:S1612": "MINOR", - "java:S1640": "MINOR", - "java:S1643": "MINOR", - "java:S1656": "MAJOR", - "java:S1659": "MINOR", - "java:S1700": "MAJOR", - "java:S1710": "MINOR", - "java:S1751": "MAJOR", - "java:S1764": "MAJOR", - "java:S1844": "MAJOR", - "java:S1845": "BLOCKER", - "java:S1849": "MAJOR", - "java:S1854": "MAJOR", - "java:S1858": "MINOR", - "java:S1860": "MAJOR", - "java:S1862": "MAJOR", - "java:S1871": "MAJOR", - "java:S1872": "MAJOR", - "java:S1874": "MINOR", - "java:S1905": "MINOR", - "java:S1940": "MINOR", - "java:S1948": "CRITICAL", - "java:S1989": "MINOR", - "java:S1994": "CRITICAL", - "java:S2053": "CRITICAL", - "java:S2055": "MINOR", - "java:S2060": "MAJOR", - "java:S2061": "MAJOR", - "java:S2062": "CRITICAL", - "java:S2065": "MINOR", - "java:S2066": "MINOR", - "java:S2068": { - "params": { - "credentialWords": "password,passwd,pwd,passphrase,java.naming.security.credentials" - }, - "severity": "BLOCKER" - }, - "java:S2077": "MAJOR", - "java:S2092": "MINOR", - "java:S2093": "CRITICAL", - "java:S2094": "MINOR", - "java:S2095": { - "params": { - "excludedResourceTypes": "" - }, - "severity": "BLOCKER" - }, - "java:S2097": "MINOR", - "java:S2109": "MAJOR", - "java:S2110": "MAJOR", - "java:S2111": "MAJOR", - "java:S2112": "MAJOR", - "java:S2114": "MAJOR", - "java:S2115": "BLOCKER", - "java:S2116": "MAJOR", - "java:S2118": "MAJOR", - "java:S2119": "CRITICAL", - "java:S2121": "MAJOR", - "java:S2122": "CRITICAL", - "java:S2123": "MAJOR", - "java:S2127": "MAJOR", - "java:S2129": "MAJOR", - "java:S2130": "MINOR", - "java:S2133": "MAJOR", - "java:S2134": "MAJOR", - "java:S2139": "MAJOR", - "java:S2140": "MINOR", - "java:S2142": "MAJOR", - "java:S2147": "MINOR", - "java:S2151": "CRITICAL", - "java:S2153": "MINOR", - "java:S2154": "MAJOR", - "java:S2157": "CRITICAL", - "java:S2159": "MAJOR", - "java:S2160": "MINOR", - "java:S2166": "MAJOR", - "java:S2167": "MINOR", - "java:S2168": "BLOCKER", - "java:S2175": "MAJOR", - "java:S2176": "CRITICAL", - "java:S2177": "MAJOR", - "java:S2178": "BLOCKER", - "java:S2183": "MINOR", - "java:S2184": "MINOR", - "java:S2185": "MAJOR", - "java:S2186": "CRITICAL", - "java:S2187": { - "params": { - "TestClassNamePattern": ".*(Test|Tests|TestCase)" - }, - "severity": "BLOCKER" - }, - "java:S2188": "BLOCKER", - "java:S2189": "BLOCKER", - "java:S2200": "MINOR", - "java:S2201": "MAJOR", - "java:S2204": "MAJOR", - "java:S2209": "MAJOR", - "java:S2222": "CRITICAL", - "java:S2225": "MAJOR", - "java:S2226": "MAJOR", - "java:S2229": "BLOCKER", - "java:S2230": "MAJOR", - "java:S2232": "MAJOR", - "java:S2234": "MAJOR", - "java:S2235": "CRITICAL", - "java:S2236": "BLOCKER", - "java:S2245": "CRITICAL", - "java:S2251": "MAJOR", - "java:S2252": "MAJOR", - "java:S2254": "CRITICAL", - "java:S2257": "CRITICAL", - "java:S2259": "MAJOR", - "java:S2272": "MINOR", - "java:S2273": "MAJOR", - "java:S2274": "CRITICAL", - "java:S2275": "BLOCKER", - "java:S2276": "BLOCKER", - "java:S2293": "MINOR", - "java:S2326": "MAJOR", - "java:S2386": "MINOR", - "java:S2387": "BLOCKER", - "java:S2388": "MAJOR", - "java:S2390": "CRITICAL", - "java:S2437": "BLOCKER", - "java:S2438": "MAJOR", - "java:S2440": "MAJOR", - "java:S2441": "MAJOR", - "java:S2442": "MAJOR", - "java:S2445": "MAJOR", - "java:S2446": "MAJOR", - "java:S2447": "CRITICAL", - "java:S2479": { - "params": { - "allowTabsInTextBlocks": "false" - }, - "severity": "CRITICAL" - }, - "java:S2583": "MAJOR", - "java:S2589": "MAJOR", - "java:S2612": "MAJOR", - "java:S2629": "MAJOR", - "java:S2637": "MINOR", - "java:S2638": "CRITICAL", - "java:S2639": "MAJOR", - "java:S2674": "MINOR", - "java:S2675": "MAJOR", - "java:S2676": "MINOR", - "java:S2677": "MAJOR", - "java:S2681": "MAJOR", - "java:S2689": "BLOCKER", - "java:S2692": "CRITICAL", - "java:S2695": "BLOCKER", - "java:S2696": "CRITICAL", - "java:S2699": { - "params": { - "customAssertionMethods": "" - }, - "severity": "BLOCKER" - }, - "java:S2718": "MAJOR", - "java:S2737": "MINOR", - "java:S2755": "BLOCKER", - "java:S2757": "MAJOR", - "java:S2761": "MAJOR", - "java:S2786": "MINOR", - "java:S2789": "MAJOR", - "java:S2864": "MAJOR", - "java:S2885": "MAJOR", - "java:S2886": "MAJOR", - "java:S2924": "MINOR", - "java:S2925": "MAJOR", - "java:S2970": "BLOCKER", - "java:S2975": "BLOCKER", - "java:S3008": { - "params": { - "format": "^[a-z][a-zA-Z0-9]*$" - }, - "severity": "MINOR" - }, - "java:S3010": "MAJOR", - "java:S3011": "MAJOR", - "java:S3012": "MINOR", - "java:S3014": "BLOCKER", - "java:S3020": "MINOR", - "java:S3034": "MAJOR", - "java:S3038": "MINOR", - "java:S3039": "MAJOR", - "java:S3042": "MAJOR", - "java:S3046": "BLOCKER", - "java:S3064": "MAJOR", - "java:S3065": "MAJOR", - "java:S3066": "MINOR", - "java:S3067": "MAJOR", - "java:S3077": "MINOR", - "java:S3078": "MAJOR", - "java:S3252": "CRITICAL", - "java:S3305": "CRITICAL", - "java:S3329": "CRITICAL", - "java:S3330": "MINOR", - "java:S3346": "MAJOR", - "java:S3358": "MAJOR", - "java:S3398": "MINOR", - "java:S3400": "MINOR", - "java:S3415": "MAJOR", - "java:S3416": "MINOR", - "java:S3436": "MAJOR", - "java:S3457": "MAJOR", - "java:S3516": "BLOCKER", - "java:S3518": "CRITICAL", - "java:S3551": "MAJOR", - "java:S3577": { - "params": { - "format": "^((Test|IT)[a-zA-Z0-9_]+|[A-Z][a-zA-Z0-9_]*(Test|Tests|TestCase|IT|ITCase))$" - }, - "severity": "MINOR" - }, - "java:S3599": "MINOR", - "java:S3626": "MINOR", - "java:S3631": "MAJOR", - "java:S3655": "MAJOR", - "java:S3740": "MAJOR", - "java:S3751": "MAJOR", - "java:S3752": "MINOR", - "java:S3753": "BLOCKER", - "java:S3776": { - "params": { - "Threshold": "15" - }, - "severity": "CRITICAL" - }, - "java:S3824": "MAJOR", - "java:S3864": "MAJOR", - "java:S3878": "MINOR", - "java:S3923": "MAJOR", - "java:S3958": "MAJOR", - "java:S3959": "MAJOR", - "java:S3972": "CRITICAL", - "java:S3973": "CRITICAL", - "java:S3981": "MAJOR", - "java:S3984": "MAJOR", - "java:S3985": "MAJOR", - "java:S3986": "MAJOR", - "java:S4032": "MINOR", - "java:S4034": "MINOR", - "java:S4036": "MINOR", - "java:S4042": "MAJOR", - "java:S4065": "MINOR", - "java:S4087": "MINOR", - "java:S4143": "MAJOR", - "java:S4144": "MAJOR", - "java:S4165": "MAJOR", - "java:S4201": "MINOR", - "java:S4274": "MAJOR", - "java:S4275": "CRITICAL", - "java:S4276": "MINOR", - "java:S4347": "CRITICAL", - "java:S4348": "MAJOR", - "java:S4349": "MINOR", - "java:S4351": "MAJOR", - "java:S4423": "CRITICAL", - "java:S4425": "MAJOR", - "java:S4426": "CRITICAL", - "java:S4433": "CRITICAL", - "java:S4434": "MAJOR", - "java:S4449": "MAJOR", - "java:S4454": "CRITICAL", - "java:S4488": "MINOR", - "java:S4502": "CRITICAL", - "java:S4507": "MINOR", - "java:S4512": "CRITICAL", - "java:S4517": "MAJOR", - "java:S4524": "CRITICAL", - "java:S4544": "CRITICAL", - "java:S4601": "CRITICAL", - "java:S4602": "BLOCKER", - "java:S4635": "CRITICAL", - "java:S4682": "MINOR", - "java:S4684": "CRITICAL", - "java:S4719": "MINOR", - "java:S4738": "MAJOR", - "java:S4790": "CRITICAL", - "java:S4830": "CRITICAL", - "java:S4838": "MINOR", - "java:S4925": "MAJOR", - "java:S4929": "MINOR", - "java:S4968": "MINOR", - "java:S4970": "CRITICAL", - "java:S4973": "MAJOR", - "java:S4977": "MINOR", - "java:S5042": "CRITICAL", - "java:S5122": "MINOR", - "java:S5164": "MAJOR", - "java:S5247": "MAJOR", - "java:S5261": "MAJOR", - "java:S5301": "MINOR", - "java:S5320": "CRITICAL", - "java:S5322": "CRITICAL", - "java:S5324": "CRITICAL", - "java:S5329": "MAJOR", - "java:S5332": "CRITICAL", - "java:S5344": "CRITICAL", - "java:S5361": "CRITICAL", - "java:S5411": "MINOR", - "java:S5413": "MAJOR", - "java:S5443": "CRITICAL", - "java:S5445": "CRITICAL", - "java:S5527": "CRITICAL", - "java:S5542": "CRITICAL", - "java:S5547": "CRITICAL", - "java:S5659": "CRITICAL", - "java:S5663": "MINOR", - "java:S5664": "MAJOR", - "java:S5665": "MINOR", - "java:S5669": "MAJOR", - "java:S5679": "MAJOR", - "java:S5689": "MINOR", - "java:S5693": { - "params": { - "fileUploadSizeLimit": "8388608" - }, - "severity": "MAJOR" - }, - "java:S5738": "MAJOR", - "java:S5776": "MAJOR", - "java:S5777": "MINOR", - "java:S5778": "MAJOR", - "java:S5779": "CRITICAL", - "java:S5783": "CRITICAL", - "java:S5785": "MAJOR", - "java:S5786": "INFO", - "java:S5790": "CRITICAL", - "java:S5803": "CRITICAL", - "java:S5804": "MAJOR", - "java:S5808": "MAJOR", - "java:S5810": "MAJOR", - "java:S5826": "CRITICAL", - "java:S5831": "MAJOR", - "java:S5833": "MAJOR", - "java:S5838": "MINOR", - "java:S5841": "MINOR", - "java:S5842": "MINOR", - "java:S5843": { - "params": { - "maxComplexity": "20" - }, - "severity": "MAJOR" - }, - "java:S5845": "CRITICAL", - "java:S5846": "CRITICAL", - "java:S5850": "MAJOR", - "java:S5852": "CRITICAL", - "java:S5853": "MINOR", - "java:S5854": "MAJOR", - "java:S5855": "MAJOR", - "java:S5856": "CRITICAL", - "java:S5857": "MINOR", - "java:S5860": "MAJOR", - "java:S5863": "MAJOR", - "java:S5866": "MAJOR", - "java:S5868": "MAJOR", - "java:S5869": "MAJOR", - "java:S5876": "CRITICAL", - "java:S5917": "MAJOR", - "java:S5958": "MAJOR", - "java:S5960": "MAJOR", - "java:S5961": { - "params": { - "MaximumAssertionNumber": "25" - }, - "severity": "MAJOR" - }, - "java:S5967": "MAJOR", - "java:S5969": "CRITICAL", - "java:S5973": "MAJOR", - "java:S5976": "MAJOR", - "java:S5993": "MAJOR", - "java:S5994": "CRITICAL", - "java:S5996": "CRITICAL", - "java:S5998": { - "params": { - "maxStackConsumptionFactor": "5.0" - }, - "severity": "MAJOR" - }, - "java:S6001": "CRITICAL", - "java:S6002": "CRITICAL", - "java:S6019": "MAJOR", - "java:S6035": "MAJOR", - "java:S6068": "MINOR", - "java:S6070": "MAJOR", - "java:S6103": "MAJOR", - "java:S6104": "CRITICAL", - "java:S6126": "MAJOR", - "java:S6201": "MINOR", - "java:S6202": "MAJOR", - "java:S6203": { - "params": { - "MaximumNumberOfLines": "5" - }, - "severity": "MINOR" - }, - "java:S6204": "MAJOR", - "java:S6205": "MINOR", - "java:S6206": "MAJOR", - "java:S6207": "MAJOR", - "java:S6208": "INFO", - "java:S6209": "CRITICAL", - "java:S6213": "MAJOR", - "java:S6216": "MAJOR", - "java:S6217": "MINOR", - "java:S6218": "MAJOR", - "java:S6219": "MINOR", - "java:S6241": "MAJOR", - "java:S6242": "MAJOR", - "java:S6243": "MAJOR", - "java:S6244": "MINOR", - "java:S6246": "MINOR", - "java:S6262": "MINOR", - "java:S6263": "MAJOR", - "java:S6288": "MAJOR", - "java:S6291": "MAJOR", - "java:S6293": "MAJOR", - "java:S6300": "MAJOR", - "java:S6301": "MAJOR", - "java:S6326": "MAJOR", - "java:S6331": "MAJOR", - "java:S6353": "MINOR", - "java:S6355": "MAJOR", - "java:S6362": "MAJOR", - "java:S6363": "MAJOR", - "java:S6373": "BLOCKER", - "java:S6376": "MAJOR", - "java:S6377": "MAJOR", - "java:S6395": "MAJOR", - "java:S6396": "MAJOR", - "java:S6397": "MAJOR", - "java:S6418": { - "params": { - "randomnessSensibility": "5.0", - "secretWords": "api[_.-]?key,auth,credential,secret,token" - }, - "severity": "BLOCKER" - }, - "java:S6432": "CRITICAL", - "java:S6437": "BLOCKER", - "java:S6485": "MAJOR", - "java:S6539": { - "params": { - "couplingThreshold": "20" - }, - "severity": "INFO" - }, - "java:S6541": { - "params": { - "cyclomaticThreshold": "15", - "locThreshold": "65", - "nestingThreshold": "3", - "noavThreshold": "7" - }, - "severity": "INFO" - }, - "java:S6548": "INFO", - "java:S6804": "MAJOR", - "java:S6806": "MAJOR", - "java:S6809": "MAJOR", - "java:S6810": "MAJOR", - "java:S6813": "MAJOR", - "java:S6814": "MAJOR", - "java:S6816": "MAJOR", - "java:S6817": "MAJOR", - "java:S6818": "MAJOR", - "java:S6829": "MINOR", - "java:S6830": "MINOR", - "java:S6831": "MAJOR", - "java:S6832": "MAJOR", - "java:S6833": "MAJOR", - "java:S6837": "MAJOR", - "java:S6838": "MAJOR", - "java:S6856": "MAJOR", - "java:S6857": "MAJOR", - "java:S6862": "MAJOR", - "java:S6863": "MAJOR", - "java:S6876": "MAJOR", - "java:S6877": "MAJOR", - "java:S6878": "MINOR", - "java:S6880": "MAJOR", - "java:S6881": "MAJOR", - "java:S6885": "MAJOR", - "java:S6889": "MAJOR", - "java:S6901": "MAJOR", - "java:S6905": "MAJOR", - "java:S6906": "MAJOR", - "java:S6909": "MAJOR", - "java:S6912": "MAJOR", - "java:S6913": "MAJOR", - "java:S6915": "MAJOR", - "java:S6916": "MAJOR", - "java:S899": "MINOR", - "javabugs:S2190": "BLOCKER", - "javabugs:S6320": "CRITICAL", - "javabugs:S6322": "CRITICAL", - "javabugs:S6416": "CRITICAL", - "javabugs:S6417": "MAJOR", - "javabugs:S6466": "CRITICAL", - "javasecurity:S2076": "BLOCKER", - "javasecurity:S2078": "BLOCKER", - "javasecurity:S2083": "BLOCKER", - "javasecurity:S2091": "BLOCKER", - "javasecurity:S2631": "CRITICAL", - "javasecurity:S3649": "BLOCKER", - "javasecurity:S5131": "BLOCKER", - "javasecurity:S5135": "BLOCKER", - "javasecurity:S5144": "MAJOR", - "javasecurity:S5145": "MINOR", - "javasecurity:S5146": "BLOCKER", - "javasecurity:S5147": "BLOCKER", - "javasecurity:S5334": "BLOCKER", - "javasecurity:S5883": "MINOR", - "javasecurity:S6096": "BLOCKER", - "javasecurity:S6173": "MAJOR", - "javasecurity:S6287": "MAJOR", - "javasecurity:S6350": "MAJOR", - "javasecurity:S6384": "BLOCKER", - "javasecurity:S6390": "CRITICAL", - "javasecurity:S6398": "MAJOR", - "javasecurity:S6399": "MAJOR", - "javasecurity:S6547": "MAJOR", - "javasecurity:S6549": "MAJOR" - } - }, "Sonar way": { "children": { "Sonar way and complexity": { @@ -3790,6 +2871,50 @@ "severity": "CRITICAL" } }, + "children": { + "With Checkstyle": { + "addedRules": { + "checkstyle:com.puppycrawl.tools.checkstyle.checks.NoCodeInFileCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.OuterTypeFilenameCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.UpperEllCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.annotation.PackageAnnotationCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidDoubleBraceInitializationCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidInlineConditionalsCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.AvoidNoArgumentSuperConstructorCallCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.ConstructorsDeclarationGroupingCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.CovariantEqualsCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.EmptyStatementCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.EqualsHashCodeCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MissingCtorCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MissingSwitchDefaultCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MultipleVariableDeclarationsCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoArrayTrailingCommaCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoCloneCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoEnumTrailingCommaCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.NoFinalizerCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.OverloadMethodsDeclarationOrderCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.ParameterAssignmentCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanExpressionCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanReturnCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.StringLiteralEqualityCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SuperCloneCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.SuperFinalizeCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.UnnecessarySemicolonInEnumerationCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.UnusedLocalVariableCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.InnerTypeLastCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocPositionCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocPackageCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.whitespace.GenericWhitespaceCheck": "MINOR", + "checkstyle:com.puppycrawl.tools.checkstyle.checks.whitespace.NoWhitespaceBeforeCaseDefaultColonCheck": "MINOR" + } + } + }, "isDefault": true } }, @@ -3798,31 +2923,6 @@ }, "jcl": { "Sonar way": { - "children": { - "Full Way": { - "addedRules": { - "jcl:S2260": "MAJOR", - "jcl:S6935": "MAJOR", - "jcl:S6942": "MAJOR", - "jcl:S6945": { - "params": { - "allowedUnconditionalSteps": "2" - }, - "severity": "MAJOR" - }, - "jcl:S6947": { - "params": { - "maxSteps": "50" - }, - "severity": "MAJOR" - }, - "jcl:Track_usage_of_rogue_programs": "INFO" - }, - "permissions": { - "groups": "sonar-users" - } - } - }, "isBuiltIn": true, "isDefault": true } @@ -3892,493 +2992,8 @@ }, "py": { "Sonar way": { - "children": { - "Olivier Way": { - "addedRules": { - "python:ClassComplexity": { - "params": { - "maximumClassComplexityThreshold": "200" - }, - "severity": "CRITICAL" - }, - "python:FunctionComplexity": { - "params": { - "maximumFunctionComplexityThreshold": "15" - }, - "severity": "CRITICAL" - }, - "python:LineLength": { - "params": { - "maximumLineLength": "120" - }, - "severity": "MAJOR" - }, - "python:LongIntegerWithLowercaseSuffixUsage": "MINOR", - "python:NoSonar": "MAJOR", - "python:OneStatementPerLine": "MAJOR", - "python:ParsingError": "MAJOR", - "python:S104": { - "params": { - "maximum": "1000" - }, - "severity": "MAJOR" - }, - "python:S1128": "MINOR", - "python:S113": "MINOR", - "python:S1131": "MINOR", - "python:S1142": { - "params": { - "max": "3" - }, - "severity": "MAJOR" - }, - "python:S134": { - "params": { - "max": "4" - }, - "severity": "CRITICAL" - }, - "python:S138": { - "params": { - "max": "100" - }, - "severity": "MAJOR" - }, - "python:S1451": { - "params": { - "headerFormat": "", - "isRegularExpression": "false" - }, - "severity": "BLOCKER" - }, - "python:S1578": { - "params": { - "format": "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" - }, - "severity": "MINOR" - }, - "python:S1707": { - "params": { - "pattern": "[ ]*\\([ _a-zA-Z0-9@.]+\\)" - }, - "severity": "MINOR" - }, - "python:S1720": "MAJOR", - "python:S1721": { - "description": "Actually in the context of an ``if``, parens are necessary:\n``if value in (\"foo\", \"bar\")`` for instance", - "severity": "MINOR", - "tags": "improve-description" - }, - "python:S1722": "MINOR", - "python:S2325": "MINOR", - "python:S2712": "BLOCKER", - "python:S3801": "MAJOR", - "python:S5856": "CRITICAL", - "python:S5953": "BLOCKER", - "python:S6538": "MAJOR", - "python:S6540": "MAJOR", - "python:S6542": "MAJOR", - "python:S6543": "MAJOR", - "python:S6545": "MINOR", - "python:S6554": "MAJOR", - "python:S6661": "MINOR", - "python:S6740": "MAJOR" - }, - "modifiedRules": { - "python:S3776": { - "params": { - "threshold": "20" - } - } - } - }, - "Pytorch way": { - "addedRules": { - "python:ClassComplexity": { - "params": { - "maximumClassComplexityThreshold": "200" - }, - "severity": "CRITICAL" - }, - "python:FunctionComplexity": { - "params": { - "maximumFunctionComplexityThreshold": "15" - }, - "severity": "CRITICAL" - }, - "python:LineLength": { - "params": { - "maximumLineLength": "120" - }, - "severity": "MAJOR" - }, - "python:LongIntegerWithLowercaseSuffixUsage": "MINOR", - "python:NoSonar": "MAJOR", - "python:OneStatementPerLine": "MAJOR", - "python:ParsingError": "MAJOR", - "python:S104": { - "params": { - "maximum": "1000" - }, - "severity": "MAJOR" - }, - "python:S1128": "MINOR", - "python:S113": "MINOR", - "python:S1131": "MINOR", - "python:S1142": { - "params": { - "max": "3" - }, - "severity": "MAJOR" - }, - "python:S134": { - "params": { - "max": "4" - }, - "severity": "CRITICAL" - }, - "python:S138": { - "params": { - "max": "100" - }, - "severity": "MAJOR" - }, - "python:S1451": { - "params": { - "headerFormat": "", - "isRegularExpression": "false" - }, - "severity": "BLOCKER" - }, - "python:S1578": { - "params": { - "format": "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" - }, - "severity": "MINOR" - }, - "python:S1721": { - "description": "Actually in the context of an ``if``, parens are necessary:\n``if value in (\"foo\", \"bar\")`` for instance", - "severity": "MINOR", - "tags": "improve-description" - }, - "python:S1722": "MINOR", - "python:S2325": "MINOR", - "python:S2712": "BLOCKER", - "python:S3801": "MAJOR", - "python:S5856": "CRITICAL", - "python:S5953": "BLOCKER", - "python:S6543": "MAJOR", - "python:S6554": "MAJOR", - "python:S6661": "MINOR", - "python:S6740": "MAJOR" - } - }, - "new": {} - }, "isBuiltIn": true, "isDefault": true - }, - "Sonar way copy": { - "children": { - "Child profile": { - "addedRules": { - "python:S2876": "BLOCKER" - } - } - }, - "rules": { - "python:BackticksUsage": "BLOCKER", - "python:ExecStatementUsage": "BLOCKER", - "python:InequalityUsage": "MAJOR", - "python:PreIncrementDecrement": "MAJOR", - "python:PrintStatementUsage": "MAJOR", - "python:S100": { - "params": { - "format": "^[a-z_][a-z0-9_]*$" - }, - "severity": "MINOR" - }, - "python:S101": { - "params": { - "format": "^_?([A-Z_][a-zA-Z0-9]*|[a-z_][a-z0-9_]*)$" - }, - "severity": "MINOR" - }, - "python:S1045": "MAJOR", - "python:S1066": "MAJOR", - "python:S107": { - "params": { - "max": "13" - }, - "severity": "MAJOR" - }, - "python:S108": "MAJOR", - "python:S1110": "MAJOR", - "python:S112": "MAJOR", - "python:S1134": "MAJOR", - "python:S1135": "INFO", - "python:S1143": "CRITICAL", - "python:S1144": "MAJOR", - "python:S116": { - "params": { - "format": "^[_a-z][_a-z0-9]*$" - }, - "severity": "MINOR" - }, - "python:S117": { - "params": { - "format": "^[_a-z][a-z0-9_]*$" - }, - "severity": "MINOR" - }, - "python:S1172": "MAJOR", - "python:S1186": "CRITICAL", - "python:S1192": { - "params": { - "exclusionRegex": "", - "threshold": "3" - }, - "severity": "CRITICAL" - }, - "python:S1226": "MINOR", - "python:S125": { - "params": { - "exception": "(fmt|py\\w+):.*" - }, - "severity": "MAJOR" - }, - "python:S1313": "MINOR", - "python:S1481": { - "params": { - "regex": "(_[a-zA-Z0-9_]*|dummy|unused|ignored)" - }, - "severity": "MINOR" - }, - "python:S1515": "MAJOR", - "python:S1542": { - "params": { - "format": "^[a-z_][a-z0-9_]*$" - }, - "severity": "MAJOR" - }, - "python:S1607": "MAJOR", - "python:S1656": "MAJOR", - "python:S1700": "MAJOR", - "python:S1716": "CRITICAL", - "python:S1751": "MAJOR", - "python:S1763": "MAJOR", - "python:S1764": "MAJOR", - "python:S1845": "BLOCKER", - "python:S1854": "MAJOR", - "python:S1862": "MAJOR", - "python:S1871": "MAJOR", - "python:S1940": "MINOR", - "python:S2053": "CRITICAL", - "python:S2068": { - "params": { - "credentialWords": "password,passwd,pwd,passphrase" - }, - "severity": "BLOCKER" - }, - "python:S2077": "MAJOR", - "python:S2092": "MINOR", - "python:S2115": "BLOCKER", - "python:S2159": "BLOCKER", - "python:S2190": "BLOCKER", - "python:S2201": "MAJOR", - "python:S2208": "CRITICAL", - "python:S2245": "CRITICAL", - "python:S2257": "CRITICAL", - "python:S2275": "BLOCKER", - "python:S2612": "MAJOR", - "python:S2638": "CRITICAL", - "python:S2710": { - "params": { - "classParameterNames": "cls,mcs,metacls" - }, - "severity": "CRITICAL" - }, - "python:S2711": "BLOCKER", - "python:S2734": "BLOCKER", - "python:S2737": { - "severity": "MINOR", - "tags": "best-practice" - }, - "python:S2755": "BLOCKER", - "python:S2757": "MAJOR", - "python:S2761": "MAJOR", - "python:S2772": "MINOR", - "python:S2823": "BLOCKER", - "python:S2836": "MAJOR", - "python:S3329": "CRITICAL", - "python:S3330": "MINOR", - "python:S3358": "MAJOR", - "python:S3403": "BLOCKER", - "python:S3457": "MAJOR", - "python:S3516": "BLOCKER", - "python:S3626": "MINOR", - "python:S3699": "MAJOR", - "python:S3752": "MINOR", - "python:S3776": { - "params": { - "threshold": "15" - }, - "severity": "CRITICAL" - }, - "python:S3827": "BLOCKER", - "python:S3862": "BLOCKER", - "python:S3923": "MAJOR", - "python:S3981": "MAJOR", - "python:S3984": "MAJOR", - "python:S3985": "MAJOR", - "python:S4143": "MAJOR", - "python:S4144": "MAJOR", - "python:S4423": "CRITICAL", - "python:S4426": "CRITICAL", - "python:S4433": "CRITICAL", - "python:S4487": { - "params": { - "enableSingleUnderscoreIssues": "false" - }, - "severity": "CRITICAL" - }, - "python:S4502": "CRITICAL", - "python:S4507": "MINOR", - "python:S4790": "CRITICAL", - "python:S4792": "CRITICAL", - "python:S4828": "CRITICAL", - "python:S4830": "CRITICAL", - "python:S5042": "CRITICAL", - "python:S5122": "MINOR", - "python:S5247": "MAJOR", - "python:S5332": "CRITICAL", - "python:S5361": "CRITICAL", - "python:S5443": "CRITICAL", - "python:S5445": "CRITICAL", - "python:S5527": "CRITICAL", - "python:S5542": "CRITICAL", - "python:S5547": "CRITICAL", - "python:S5549": "BLOCKER", - "python:S5603": "MAJOR", - "python:S5607": "BLOCKER", - "python:S5632": "BLOCKER", - "python:S5644": "BLOCKER", - "python:S5655": "CRITICAL", - "python:S5659": "CRITICAL", - "python:S5685": "MINOR", - "python:S5704": "CRITICAL", - "python:S5706": "MAJOR", - "python:S5707": "CRITICAL", - "python:S5708": "BLOCKER", - "python:S5709": "CRITICAL", - "python:S5712": "CRITICAL", - "python:S5713": "MINOR", - "python:S5714": "BLOCKER", - "python:S5717": "CRITICAL", - "python:S5719": "BLOCKER", - "python:S5720": { - "params": { - "ignoredDecorators": "abstractmethod" - }, - "severity": "CRITICAL" - }, - "python:S5722": "BLOCKER", - "python:S5724": "BLOCKER", - "python:S5727": "CRITICAL", - "python:S5747": "CRITICAL", - "python:S5754": "CRITICAL", - "python:S5756": "BLOCKER", - "python:S5780": "MAJOR", - "python:S5781": "MAJOR", - "python:S5795": "MAJOR", - "python:S5796": "MAJOR", - "python:S5797": "CRITICAL", - "python:S5799": "MAJOR", - "python:S5806": "MAJOR", - "python:S5807": "BLOCKER", - "python:S5828": "BLOCKER", - "python:S5842": "MINOR", - "python:S5843": { - "params": { - "maxComplexity": "20" - }, - "severity": "MAJOR" - }, - "python:S5845": "CRITICAL", - "python:S5850": "MAJOR", - "python:S5855": "MAJOR", - "python:S5857": "MINOR", - "python:S5864": "MAJOR", - "python:S5868": "MAJOR", - "python:S5869": "MAJOR", - "python:S5886": "MAJOR", - "python:S5890": "MAJOR", - "python:S5905": "BLOCKER", - "python:S5914": "MAJOR", - "python:S5996": "CRITICAL", - "python:S6002": "CRITICAL", - "python:S6019": "MAJOR", - "python:S6035": "MAJOR", - "python:S6245": "MINOR", - "python:S6252": "MINOR", - "python:S6265": "BLOCKER", - "python:S6270": "BLOCKER", - "python:S6275": "MAJOR", - "python:S6281": "CRITICAL", - "python:S6302": "BLOCKER", - "python:S6303": "MAJOR", - "python:S6304": "BLOCKER", - "python:S6308": "MAJOR", - "python:S6317": "CRITICAL", - "python:S6319": "MAJOR", - "python:S6321": "MINOR", - "python:S6323": "MAJOR", - "python:S6326": "MAJOR", - "python:S6327": "MAJOR", - "python:S6328": "MAJOR", - "python:S6329": "BLOCKER", - "python:S6330": "MAJOR", - "python:S6331": "MAJOR", - "python:S6332": "MAJOR", - "python:S6333": "BLOCKER", - "python:S6353": "MINOR", - "python:S6395": "MAJOR", - "python:S6396": "MAJOR", - "python:S6397": "MAJOR", - "python:S6463": "MAJOR", - "python:S6468": "MAJOR", - "python:S905": { - "params": { - "ignoredOperators": "<<,>>,|", - "reportOnStrings": "false" - }, - "severity": "MAJOR" - }, - "python:S930": "BLOCKER", - "pythonbugs:S2259": "MAJOR", - "pythonbugs:S2583": "MAJOR", - "pythonbugs:S2589": "MAJOR", - "pythonbugs:S5633": "BLOCKER", - "pythonbugs:S6417": "MAJOR", - "pythonbugs:S6464": "CRITICAL", - "pythonbugs:S6465": "CRITICAL", - "pythonbugs:S6466": "CRITICAL", - "pythonsecurity:S2076": "BLOCKER", - "pythonsecurity:S2078": "BLOCKER", - "pythonsecurity:S2083": "BLOCKER", - "pythonsecurity:S2091": "BLOCKER", - "pythonsecurity:S2631": "CRITICAL", - "pythonsecurity:S3649": "BLOCKER", - "pythonsecurity:S5131": "BLOCKER", - "pythonsecurity:S5135": "BLOCKER", - "pythonsecurity:S5144": "MAJOR", - "pythonsecurity:S5145": "MINOR", - "pythonsecurity:S5146": "BLOCKER", - "pythonsecurity:S5147": "BLOCKER", - "pythonsecurity:S5334": "BLOCKER", - "pythonsecurity:S6287": "MAJOR", - "pythonsecurity:S6350": "MAJOR" - } } }, "rpg": { @@ -4496,14 +3111,6 @@ } }, "instantiated": { - "java:Don_t_be_rude": { - "params": { - "message": "", - "regularExpression": "(fuck|shit|merde)" - }, - "severity": "MAJOR", - "templateKey": "java:S124" - }, "jcl:Track_usage_of_rogue_programs": { "params": { "programName": "ROGUEPROG", @@ -4538,21 +3145,20 @@ } }, "users": { - "TEMP": { + "TEMP_ADMIN": { "local": true, - "name": "User name TEMP", - "scmAccounts": [ - "jdoe@gmail.com,jdoe@acme.com,john.doe@acme.com,jdoe@live.com" - ] + "name": "User name TEMP_ADMIN" }, "admin": { "email": "admin@acme.com", - "groups": "sonar-administrators", + "groups": "sonar-administrators, tech-leads", "local": true, "name": "Administrator", - "scmAccounts": [ - "admin-acme,administrator-acme" - ] + "scmAccounts": "admin-acme, administrator-acme" + }, + "ado": { + "local": true, + "name": "Azure DevOps Service Account" }, "bbTEMPaa": { "local": true, diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 6ae79a994..133cf15b0 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -46,6 +46,14 @@ def create_test_object(a_class: type, key: str) -> any: return o +@pytest.fixture(autouse=True) +def run_around_tests(): + util.start_logging() + url = util.TEST_SQ.url + yield + util.TEST_SQ.url = url + + @pytest.fixture def get_test_project() -> Generator[projects.Project]: """setup of tests""" diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py new file mode 100644 index 000000000..4960bab61 --- /dev/null +++ b/test/unit/test_cli.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +""" projects_cli tests """ + +from collections.abc import Generator +import json +import logging +import pytest +from sonar import exceptions, projects, utilities as sutil +from cli import options as opt +import utilities as util +import cli.projects_cli as proj_cli + + +def test_import_compatibility() -> None: + util.start_logging() + jsondata = util.SQ.basics() + assert proj_cli.__check_sq_environments(util.SQ, jsondata) is None + + incompatible_data = jsondata.copy() + incompatible_data["version"] = "8.7.0" + with pytest.raises(exceptions.UnsupportedOperation): + proj_cli.__check_sq_environments(util.SQ, incompatible_data) + + incompatible_data = jsondata.copy() + incompatible_data["plugins"] = {} + with pytest.raises(exceptions.UnsupportedOperation): + proj_cli.__check_sq_environments(util.SQ, incompatible_data) + + incompatible_data["plugins"] = jsondata["plugins"].copy() + incompatible_data["plugins"]["lua"] = "1.0 [Lua Analyzer]" + with pytest.raises(exceptions.UnsupportedOperation): + proj_cli.__check_sq_environments(util.SQ, incompatible_data) + + incompatible_data["plugins"] = jsondata["plugins"].copy() + for p, v in incompatible_data["plugins"].items(): + incompatible_data["plugins"][p] = "1." + v + with pytest.raises(exceptions.UnsupportedOperation): + proj_cli.__check_sq_environments(util.SQ, incompatible_data) + + +def test_import(get_json_file: Generator[str]) -> None: + """test_import""" + export_file = get_json_file + cmd = f"projects_cli.py {util.SQS_OPTS} --{opt.EXPORT} --{opt.REPORT_FILE} {export_file} --{opt.KEYS} project1" + util.run_success_cmd(proj_cli.main, cmd) + + if util.SQ.version() == util.TEST_SQ.version(): + assert proj_cli.__import_projects(util.TEST_SQ, file=export_file) is None + + with open(export_file, "r", encoding="utf-8") as fd: + data = json.load(fd) + data["project_exports"][0]["key"] = "TEMP-IMPORT_PROJECT-KEY" + with open(export_file, "w", encoding="utf-8") as fd: + print(json.dumps(data), file=fd) + assert proj_cli.__import_projects(util.TEST_SQ, file=export_file) is None + proj = projects.Project.get_object(util.TEST_SQ, "TEMP-IMPORT_PROJECT-KEY") + proj.delete() + + # Mess up the JSON file and retry import + with open(export_file, "r", encoding="utf-8") as fd: + data = fd.read() + data += "]" + with open(export_file, "w", encoding="utf-8") as fd: + print(data, file=fd) + with pytest.raises(opt.ArgumentsError): + proj_cli.__import_projects(util.TEST_SQ, file=export_file) diff --git a/test/unit/test_groups.py b/test/unit/test_groups.py index 72da21434..08807ae97 100644 --- a/test/unit/test_groups.py +++ b/test/unit/test_groups.py @@ -94,7 +94,10 @@ def test_remove_non_existing_user(get_test_group: Generator[groups.Group], get_t util.start_logging() gr = get_test_group u = get_test_user - gr.remove_user(u) + try: + gr.remove_user(u) + except exceptions.ObjectNotFound: + pass gr.add_user(u) u.id = util.NON_EXISTING_KEY u.login = util.NON_EXISTING_KEY diff --git a/test/unit/test_issues.py b/test/unit/test_issues.py index 4f3b0e166..9931ffa71 100644 --- a/test/unit/test_issues.py +++ b/test/unit/test_issues.py @@ -22,6 +22,8 @@ """ Test of the issues module and class, as well as changelog """ from datetime import datetime +import pytest + import utilities as tutil from sonar import issues from sonar import utilities as util @@ -59,12 +61,12 @@ def test_issue() -> None: issue2 = issues_d[issue_key_accepted] assert not issue.almost_identical_to(issue2) + assert f"{issue}".startswith(f"Key: {issue.key} - Type:") + def test_add_comments() -> None: """Test issue comments manipulations""" - issues_d = issues.search_by_project( - endpoint=tutil.SQ, project_key="pytorch", params={"impactSeverities": "HIGH", "impactSoftwareQualities": "RELIABILITY"} - ) + issues_d = issues.search_by_project(endpoint=tutil.SQ, project_key="project1") issue = list(issues_d.values())[0] comment = f"NOW is {str(datetime.now())}" assert issue.add_comment(comment) @@ -161,3 +163,74 @@ def test_changelog() -> None: assert changelog.get_tags() is None (t, _) = changelog.changelog_type() assert t == "FALSE-POSITIVE" + + +def test_request_error() -> None: + """test_request_error""" + issues_d = issues.search_by_project(endpoint=tutil.TEST_SQ, project_key="project1") + issue = list(issues_d.values())[0] + tutil.TEST_SQ.url = "http://localhost:3337" + assert not issue.add_comment("Won't work") + + assert not issue.assign("admin") + + +def test_transitions() -> None: + """test_transitions""" + issues_d = issues.search_by_project(endpoint=tutil.SQ, project_key="project1") + issue = list(issues_d.values())[0] + + assert issue.confirm() + assert not issue.confirm() + assert issue.unconfirm() + assert not issue.unconfirm() + + assert issue.resolve_as_fixed() + assert not issue.resolve_as_fixed() + assert issue.reopen() + assert not issue.reopen() + + assert issue.mark_as_wont_fix() + assert not issue.mark_as_wont_fix() + assert issue.reopen() + assert not issue.reopen() + + assert issue.accept() + assert not issue.accept() + assert issue.reopen() + assert not issue.reopen() + + assert issue.mark_as_false_positive() + assert not issue.mark_as_false_positive() + assert issue.reopen() + assert not issue.reopen() + + +def test_search_first() -> None: + """test_search_first""" + assert issues.search_first(tutil.SQ, components="non-existing-project-key") is None + + +def test_get_facets() -> None: + """test_get_facets""" + facets = issues._get_facets(tutil.SQ, project_key="okorach_sonar-tools") + assert len(facets["directories"]) > 1 + + +def test_search_by_small() -> None: + """Test search_by on small project (less than 10000 issues)""" + list1 = issues.search_by_project(tutil.SQ, "okorach_sonar-tools") + params = {"components": "okorach_sonar-tools", "project": "okorach_sonar-tools"} + assert list1 == issues.search_by_type(tutil.SQ, params) + assert list1 == issues.search_by_severity(tutil.SQ, params) + assert list1 == issues.search_by_date(tutil.SQ, params) + assert list1 == issues.search_by_directory(tutil.SQ, params) + + +def test_search_by_large() -> None: + """Test search_by on large project (more than 10000 issues)""" + assert len(issues.search_by_project(tutil.SQ, "pytorch")) > 10000 + + params = {"components": "pytorch", "project": "pytorch"} + with pytest.raises(issues.TooManyIssuesError): + issues.search_by_severity(tutil.SQ, params) diff --git a/test/unit/test_platform.py b/test/unit/test_platform.py new file mode 100644 index 000000000..389a28ee6 --- /dev/null +++ b/test/unit/test_platform.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +""" platform tests """ + +import json +from requests import RequestException + +import pytest +import utilities as util +from sonar import platform, settings + + +def test_system_id() -> None: + server_id = util.SQ.server_id() + assert server_id == util.SQ.server_id() + assert server_id == util.SQ.server_id() + + +def test_db() -> None: + assert util.SC.database().lower() == "postgresql" + assert util.SQ.database().lower() == "postgresql" + + +def test_plugins() -> None: + assert util.SC.plugins() == {} + + +def test_get_set_reset_settings() -> None: + # util.start_logging() + assert util.SQ.reset_setting("sonar.exclusions") + assert util.SQ.get_setting("sonar.exclusions") is None + + assert util.SQ.set_setting("sonar.exclusions", "**/*.foo") + assert util.SQ.get_setting("sonar.exclusions") == ["**/*.foo"] + + assert util.SQ.set_setting("sonar.exclusions", "**/*.foo,**/*.bar") + assert util.SQ.get_setting("sonar.exclusions") == ["**/*.foo", "**/*.bar"] + + assert util.SQ.reset_setting("sonar.exclusions") + assert util.SQ.get_setting("sonar.exclusions") is None + + +def test_import() -> None: + with open("test/files/config.json", "r", encoding="utf-8") as f: + json_config = json.load(f) + json_config["globalSettings"]["generalSettings"][settings.NEW_CODE_PERIOD] = 60 + assert platform.import_config(util.TEST_SQ, json_config) is None + + json_config.pop("globalSettings") + assert util.TEST_SQ.import_config(json_config) is None + + +def test_sys_info() -> None: + data = util.SC.sys_info() + assert data == {"System": {"Server ID": "sonarcloud"}} + + data = util.SQ.sys_info() + assert "System" in data + + +def test_wrong_url() -> None: + util.TEST_SQ.url = "http://localhost:3337" + + util.TEST_SQ._sys_info = None + with pytest.raises(RequestException): + util.TEST_SQ.sys_info() + + util.TEST_SQ.global_permissions() + + +def test_set_webhooks() -> None: + assert util.SQ.set_webhooks(None) is None + + +def test_normalize_api() -> None: + normalized_result = "/api/projects/search" + for input in "/projects/search", "/api/projects/search", "api/projects/search", "projects/search": + assert platform._normalize_api(input) == normalized_result + + +def test_convert_for_yaml() -> None: + with open("test/files/config.json", "r", encoding="utf-8") as f: + json_config = json.load(f)["globalSettings"] + yaml_json = platform.convert_for_yaml(json_config.copy()) + assert len(yaml_json) == len(json_config) diff --git a/test/unit/test_project_export.py b/test/unit/test_project_export.py index b98c473e6..11d69698b 100644 --- a/test/unit/test_project_export.py +++ b/test/unit/test_project_export.py @@ -22,9 +22,10 @@ """ sonar-projects tests """ -import os import sys from collections.abc import Generator +import pytest +from unittest.mock import patch import utilities as util from sonar import errcodes @@ -74,3 +75,21 @@ def test_export_sq_cloud(get_json_file: Generator[str]) -> None: def test_import_no_file() -> None: """test_import_no_file""" util.run_failed_cmd(projects_cli.main, f"{OPTS} --{opt.IMPORT}", errcodes.ARGS_ERROR) + + +def test_no_export_or_import(get_json_file: Generator[str]) -> None: + """test_no_export_or_import""" + args = f"{OPTS} --{opt.REPORT_FILE} {get_json_file}" + with pytest.raises(SystemExit) as e: + with patch.object(sys, "argv", args.split(" ")): + projects_cli.main() + assert int(str(e.value)) == errcodes.ARGS_ERROR + + +def test_no_import_file() -> None: + """test_no_import_file""" + args = f"{OPTS} --{opt.REPORT_FILE} non-existing.json" + with pytest.raises(SystemExit) as e: + with patch.object(sys, "argv", args.split(" ")): + projects_cli.main() + assert int(str(e.value)) == errcodes.ARGS_ERROR diff --git a/test/unit/test_sonarcloud.py b/test/unit/test_sonarcloud.py index d36c52182..2dbfa9706 100644 --- a/test/unit/test_sonarcloud.py +++ b/test/unit/test_sonarcloud.py @@ -126,3 +126,8 @@ def test_org_search_sq() -> None: with pytest.raises(exceptions.UnsupportedOperation): _ = organizations.get_list(endpoint=util.SQ) + + +def test_audit() -> None: + """test_audit""" + util.SC.audit({}) From e31731a8d5739c0d472a00c7998a5ab0ed63bcdc Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Thu, 9 Jan 2025 17:04:57 +0100 Subject: [PATCH 21/22] CE and DE better support (#1556) * Exclude temp test files * Add credentials for LATEST CE and LTS CE * Add tests for CE * Fix generator for apps and portfolios in lower editions * Fixes for CE and DE * Fix for support of CE and DE config export * Safety record login in to_json() * Fix for CE * Formatting * CE support * CE support * Adapt tests to CE * Avoid repeating soanrcloud tests * Add latest-ce and lts-ce tests * Adjust tests to CE * Adjust tests to CE * Adjusts tests to CE * Fix check for edition * Add more tests * Fix for sonarcloud * Fix for SonarCloud * Fix for SonarCloud --- .gitignore | 4 + cli/config.py | 18 +-- conf/prep_tests.sh | 8 +- conf/run_tests.sh | 2 +- sonar/audit/config.py | 2 +- sonar/platform.py | 3 + sonar/users.py | 1 + test/unit/conftest.py | 89 +++++++-------- test/unit/credentials-latest-ce.py | 25 +++++ test/unit/credentials-lts-ce.py | 25 +++++ test/unit/test_apps.py | 170 +++++++++++++++-------------- test/unit/test_devops.py | 4 +- test/unit/test_migration.py | 39 +++---- test/unit/test_portfolios.py | 44 ++++---- test/unit/test_projects.py | 42 +++++-- test/unit/test_qp.py | 2 +- test/unit/test_sqobject.py | 7 ++ test/unit/utilities.py | 2 +- 18 files changed, 294 insertions(+), 193 deletions(-) create mode 100644 test/unit/credentials-latest-ce.py create mode 100644 test/unit/credentials-lts-ce.py diff --git a/.gitignore b/.gitignore index 1371b48f8..76d8d5f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ tmp/ test/lts/ test/latest/ test/cloud/ +test/latest-ce/ +test/lts-ce/ +test/latest-de/ +test/lts-de/ diff --git a/cli/config.py b/cli/config.py index 6b885f4c2..ccb683152 100644 --- a/cli/config.py +++ b/cli/config.py @@ -184,18 +184,20 @@ def export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> Non if what_item not in what: continue ndx, func, _ = call_data + if not is_first: + print(",", file=fd) + is_first = False + worker = Thread(target=write_objects, args=(write_q, fd, ndx, export_settings)) + worker.daemon = True + worker.name = f"Write{ndx[:1].upper()}{ndx[1:10]}" + worker.start() try: - if not is_first: - print(",", file=fd) - is_first = False - worker = Thread(target=write_objects, args=(write_q, fd, ndx, export_settings)) - worker.daemon = True - worker.name = f"Write{ndx[:1].upper()}{ndx[1:10]}" - worker.start() func(endpoint, export_settings=export_settings, key_list=kwargs[options.KEYS], write_q=write_q) - write_q.join() except exceptions.UnsupportedOperation as e: log.warning(e.message) + if write_q: + write_q.put(utilities.WRITE_END) + write_q.join() print("\n}", file=fd) remove_empty = False if mode == "MIGRATION" else not kwargs.get(EXPORT_EMPTY, False) utilities.normalize_json_file(file, remove_empty=remove_empty, remove_none=True) diff --git a/conf/prep_tests.sh b/conf/prep_tests.sh index a5a7e4265..699987ee6 100755 --- a/conf/prep_tests.sh +++ b/conf/prep_tests.sh @@ -23,7 +23,10 @@ ROOTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" cd "$ROOTDIR/test/unit" || exit 1 -for target in lts latest +echo "" +echo "Generating edition / version specific tests" + +for target in lts latest latest-ce lts-ce do rm -rf "$ROOTDIR/test/$target" mkdir -p "$ROOTDIR/test/$target" 2>/dev/null @@ -35,4 +38,7 @@ do cp "credentials-$target.py" "$ROOTDIR/test/$target/credentials.py" mv "$ROOTDIR/test/$target/conftest_${target}.py" "$ROOTDIR/test/$target/conftest.py" mv "$ROOTDIR/test/$target/utilities_${target}.py" "$ROOTDIR/test/$target/utilities.py" + if [ "$target" != "latest" ]; then + rm "$ROOTDIR/test/$target/"test_sonarcloud*.py + fi done diff --git a/conf/run_tests.sh b/conf/run_tests.sh index 32b7e7594..8d4a0d73d 100755 --- a/conf/run_tests.sh +++ b/conf/run_tests.sh @@ -32,7 +32,7 @@ echo "Running tests" export SONAR_HOST_URL=${1:-${SONAR_HOST_URL}} -for target in latest lts cloud +for target in latest lts latest-ce lts-ce cloud do if [ -d "$ROOTDIR/test/$target/" ]; then coverage run --branch --source="$ROOTDIR" -m pytest "$ROOTDIR/test/$target/" --junit-xml="$buildDir/xunit-results-$target.xml" diff --git a/sonar/audit/config.py b/sonar/audit/config.py index 47a42fdef..7cb2de688 100644 --- a/sonar/audit/config.py +++ b/sonar/audit/config.py @@ -76,7 +76,7 @@ def get_property(name: str, settings: Optional[types.ConfigSettings] = None) -> """Returns the value of a given property""" if settings is None: settings = _CONFIG_SETTINGS - return settings.get(name, "") + return "" if not settings else settings.get(name, "") def configure() -> None: diff --git a/sonar/platform.py b/sonar/platform.py index 793e79ceb..8b01e18f3 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -570,6 +570,9 @@ def _audit_logs(self, audit_settings: types.ConfigSettings) -> list[Problem]: if not audit_settings.get("audit.logs", True): log.info("Logs audit is disabled, skipping logs audit...") return [] + if self.is_sonarcloud(): + log.info("Logs audit not available with SonarQube Cloud, skipping logs audit...") + return [] log_map = {"app": "sonar.log", "ce": "ce.log", "web": "web.log", "es": "es.log"} if self.edition() == "datacenter": log_map.pop("es") diff --git a/sonar/users.py b/sonar/users.py index 38cbe7deb..5b43a2600 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -452,6 +452,7 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr :rtype: dict """ json_data = self.sq_json.copy() + json_data["login"] = self.login json_data["scmAccounts"] = self.scm_accounts json_data["groups"] = self.groups().copy() if export_settings.get("MODE", "") == "MIGRATION": diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 133cf15b0..898bb95db 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -38,7 +38,6 @@ def create_test_object(a_class: type, key: str) -> any: """Creates a SonarQube test object of a given class""" - util.start_logging() try: o = a_class.get_object(endpoint=util.SQ, key=key) except exceptions.ObjectNotFound: @@ -70,55 +69,67 @@ def get_test_project() -> Generator[projects.Project]: @pytest.fixture def get_test_app() -> Generator[applications.Application]: """setup of tests""" - o = create_test_object(applications.Application, key=util.TEMP_KEY) + o = None + if util.SQ.edition() in ("developer", "enterprise", "datacenter"): + o = create_test_object(applications.Application, key=util.TEMP_KEY) yield o - o.key = util.TEMP_KEY - try: - o.delete() - except exceptions.ObjectNotFound: - pass + if util.SQ.edition() in ("developer", "enterprise", "datacenter"): + o.key = util.TEMP_KEY + try: + o.delete() + except exceptions.ObjectNotFound: + pass @pytest.fixture def get_test_portfolio() -> Generator[portfolios.Portfolio]: """setup of tests""" - o = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY) + o = None + if util.SQ.edition() in ("enterprise", "datacenter"): + o = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY) yield o - o.key = util.TEMP_KEY - try: - o.delete() - except exceptions.ObjectNotFound: - pass + if util.SQ.edition() in ("enterprise", "datacenter"): + o.key = util.TEMP_KEY + try: + o.delete() + except exceptions.ObjectNotFound: + pass @pytest.fixture def get_test_portfolio_2() -> Generator[portfolios.Portfolio]: """setup of tests""" - o = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY_2) + o = None + if util.SQ.edition() in ("enterprise", "datacenter"): + o = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY_2) yield o - o.key = util.TEMP_KEY_2 - try: - o.delete() - except exceptions.ObjectNotFound: - pass + if util.SQ.edition() in ("enterprise", "datacenter"): + o.key = util.TEMP_KEY_2 + try: + o.delete() + except exceptions.ObjectNotFound: + pass @pytest.fixture def get_test_subportfolio() -> Generator[portfolios.Portfolio]: """setup of tests""" - parent = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY) - subp = parent.add_standard_subportfolio(key=util.TEMP_KEY_3, name=util.TEMP_KEY_3) + subp = None + if util.SQ.edition() in ("enterprise", "datacenter"): + parent = create_test_object(portfolios.Portfolio, key=util.TEMP_KEY) + subp = parent.add_standard_subportfolio(key=util.TEMP_KEY_3, name=util.TEMP_KEY_3) yield subp - subp.key = util.TEMP_KEY_3 - try: - subp.delete() - except exceptions.ObjectNotFound: - pass - parent.key = util.TEMP_KEY - try: - parent.delete() - except exceptions.ObjectNotFound: - pass + if util.SQ.edition() in ("enterprise", "datacenter"): + subp.key = util.TEMP_KEY_3 + try: + subp.delete() + except exceptions.ObjectNotFound: + pass + parent.key = util.TEMP_KEY + try: + parent.delete() + except exceptions.ObjectNotFound: + pass @pytest.fixture @@ -216,22 +227,6 @@ def get_sarif_file() -> Generator[str]: rm(file) -@pytest.fixture -def get_test_application() -> Generator[applications.Application]: - """setup of tests""" - util.start_logging() - try: - o = applications.Application.get_object(endpoint=util.SQ, key=util.TEMP_KEY) - except exceptions.ObjectNotFound: - o = applications.Application.create(endpoint=util.SQ, key=util.TEMP_KEY, name=util.TEMP_KEY) - yield o - try: - o.key = util.TEMP_KEY - o.delete() - except exceptions.ObjectNotFound: - pass - - @pytest.fixture def get_test_quality_gate() -> Generator[qualitygates.QualityGate]: """setup of tests""" diff --git a/test/unit/credentials-latest-ce.py b/test/unit/credentials-latest-ce.py new file mode 100644 index 000000000..96b6cfd18 --- /dev/null +++ b/test/unit/credentials-latest-ce.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "http://localhost:10001" +TARGET_TOKEN = getenv("SONAR_TOKEN_LATEST_ADMIN_USER") diff --git a/test/unit/credentials-lts-ce.py b/test/unit/credentials-lts-ce.py new file mode 100644 index 000000000..e00cca5c3 --- /dev/null +++ b/test/unit/credentials-lts-ce.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# +# sonar-tools tests +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +from os import getenv + +TARGET_PLATFORM = "http://localhost:9001" +TARGET_TOKEN = getenv("SONAR_TOKEN_LTS_ADMIN_USER") diff --git a/test/unit/test_apps.py b/test/unit/test_apps.py index 11650b5fd..d04534a79 100644 --- a/test/unit/test_apps.py +++ b/test/unit/test_apps.py @@ -37,7 +37,7 @@ def test_get_object() -> None: """Test get_object and verify that if requested twice the same object is returned""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) else: @@ -50,7 +50,7 @@ def test_get_object() -> None: def test_count() -> None: """Verify count works""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.count(util.SQ) else: @@ -59,7 +59,7 @@ def test_count() -> None: def test_search() -> None: """Verify that search with criterias work""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.search(endpoint=util.SQ, params={"s": "analysisDate"}) else: @@ -85,7 +85,7 @@ def test_get_object_non_existing() -> None: def test_exists(get_test_app) -> None: """Test exist""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.exists(endpoint=util.SQ, key=EXISTING_KEY) with pytest.raises(exceptions.UnsupportedOperation): @@ -99,7 +99,7 @@ def test_exists(get_test_app) -> None: def test_get_list() -> None: """Test portfolio get_list""" k_list = [EXISTING_KEY, EXISTING_KEY_2] - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.get_list(endpoint=util.SQ, key_list=k_list) else: @@ -109,98 +109,100 @@ def test_get_list() -> None: def test_create_delete() -> None: """Test portfolio create delete""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) - else: - app = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) - assert app is not None - assert app.key == util.TEMP_KEY - assert app.name == util.TEMP_NAME - app.delete() - assert not applications.exists(endpoint=util.SQ, key=util.TEMP_KEY) + return + app = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) + assert app is not None + assert app.key == util.TEMP_KEY + assert app.name == util.TEMP_NAME + app.delete() + assert not applications.exists(endpoint=util.SQ, key=util.TEMP_KEY) - # Test delete with 1 project in the app - app = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) - app.add_projects(["okorach_sonar-tools"]) - app.delete() - assert not applications.exists(endpoint=util.SQ, key=util.TEMP_KEY) + # Test delete with 1 project in the app + app = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) + app.add_projects(["okorach_sonar-tools"]) + app.delete() + assert not applications.exists(endpoint=util.SQ, key=util.TEMP_KEY) def test_permissions_1(get_test_app) -> None: """Test permissions""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.create(endpoint=util.SQ, name="An app", key=TEST_KEY) - else: - app = get_test_app - app.set_permissions({"groups": {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]}}) - # assert app.permissions().to_json()["groups"] == {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]} + return + app = get_test_app + app.set_permissions({"groups": {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]}}) + # assert app.permissions().to_json()["groups"] == {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]} def test_permissions_2(get_test_app) -> None: """Test permissions""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.create(endpoint=util.SQ, name=util.TEMP_NAME, key=util.TEMP_KEY) - else: - app = get_test_app - app.set_permissions({"groups": {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]}}) - # assert app.permissions().to_json()["groups"] == {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]} + return + app = get_test_app + app.set_permissions({"groups": {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]}}) + # assert app.permissions().to_json()["groups"] == {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]} def test_get_projects() -> None: """test_get_projects""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - else: - app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - count = len(app.projects()) - assert count > 0 - assert len(app.projects()) == count + return + app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) + count = len(app.projects()) + assert count > 0 + assert len(app.projects()) == count def test_get_branches() -> None: """test_get_projects""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - else: - app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - count = len(app.branches()) - assert count > 0 - assert len(app.branches()) == count + return + app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) + count = len(app.branches()) + assert count > 0 + assert len(app.branches()) == count def test_no_audit() -> None: """Check stop fast when audit params are disabled""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - else: - app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - assert len(app.audit({"audit.applications": False})) == 0 - assert len(app._audit_empty({"audit.applications.empty": False})) == 0 - assert len(app._audit_singleton({"audit.applications.singleton": False})) == 0 + return + app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) + assert len(app.audit({"audit.applications": False})) == 0 + assert len(app._audit_empty({"audit.applications.empty": False})) == 0 + assert len(app._audit_singleton({"audit.applications.singleton": False})) == 0 def test_search_by_name() -> None: """test_search_by_name""" - if util.SQ.edition() == "community": + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): with pytest.raises(exceptions.UnsupportedOperation): _ = applications.search_by_name(endpoint=util.SQ, name="TEST_APP") - else: - app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) - other_apps = applications.search_by_name(endpoint=util.SQ, name=app.name) + return + app = applications.Application.get_object(endpoint=util.SQ, key=EXISTING_KEY) + other_apps = applications.search_by_name(endpoint=util.SQ, name=app.name) - assert len(other_apps) == 1 - first_app = list(other_apps.values())[0] - assert app == first_app + assert len(other_apps) == 1 + first_app = list(other_apps.values())[0] + assert app == first_app def test_set_tags(get_test_app: Generator[applications.Application]) -> None: """test_set_tags""" + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") o = get_test_app assert o.set_tags(util.TAGS) @@ -214,39 +216,44 @@ def test_set_tags(get_test_app: Generator[applications.Application]) -> None: def test_not_found(get_test_app: Generator[applications.Application]) -> None: """test_not_found""" - if util.SQ.edition() != "community": - o = get_test_app - o.key = "mess-me-up" - with pytest.raises(exceptions.ObjectNotFound): - o.refresh() + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + o = get_test_app + o.key = "mess-me-up" + with pytest.raises(exceptions.ObjectNotFound): + o.refresh() def test_already_exists(get_test_app: Generator[applications.Application]) -> None: - if util.SQ.edition() != "community": - app = get_test_app - with pytest.raises(exceptions.ObjectAlreadyExists): - _ = applications.Application.create(endpoint=util.SQ, key=app.key, name="Foo Bar") + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + app = get_test_app + with pytest.raises(exceptions.ObjectAlreadyExists): + _ = applications.Application.create(endpoint=util.SQ, key=app.key, name="Foo Bar") def test_branch_exists(get_test_app: Generator[applications.Application]) -> None: - if util.SQ.edition() != "community": - app = get_test_app - assert app.branch_exists("main") - assert not app.branch_exists("non-existing") + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + app = get_test_app + assert app.branch_exists("main") + assert not app.branch_exists("non-existing") def test_branch_is_main(get_test_app: Generator[applications.Application]) -> None: - if util.SQ.edition() != "community": - app = get_test_app - assert app.branch_is_main("main") - with pytest.raises(exceptions.ObjectNotFound): - app.branch_is_main("non-existing") + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + app = get_test_app + assert app.branch_is_main("main") + with pytest.raises(exceptions.ObjectNotFound): + app.branch_is_main("non-existing") def test_get_issues(get_test_app: Generator[applications.Application]) -> None: - if util.SQ.edition() != "community": - app = get_test_app - assert len(app.get_issues()) == 0 + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + app = get_test_app + assert len(app.get_issues()) == 0 def test_audit_disabled() -> None: @@ -254,8 +261,10 @@ def test_audit_disabled() -> None: assert len(applications.audit(util.SQ, {"audit.applications": False})) == 0 -def test_app_branches(get_test_application: Generator[applications.Application]) -> None: - app = get_test_application +def test_app_branches(get_test_app: Generator[applications.Application]) -> None: + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + app = get_test_app definition = { "branches": { "Other Branch": {"projects": {"TESTSYNC": "some-branch", "demo:jcl": "main", "training:security": "main"}}, @@ -280,7 +289,8 @@ def test_app_branches(get_test_application: Generator[applications.Application]) def test_convert_for_yaml() -> None: - if util.SQ.edition() != "community": - data = applications.export(util.SQ, {}) - yaml_list = applications.convert_for_yaml(data) - assert len(yaml_list) == len(data) + if util.SQ.edition() not in ("developer", "enterprise", "datacenter"): + pytest.skip("Apps unsupported in SonarQube Community Build and SonarQube Cloud") + data = applications.export(util.SQ, {}) + yaml_list = applications.convert_for_yaml(data) + assert len(yaml_list) == len(data) diff --git a/test/unit/test_devops.py b/test/unit/test_devops.py index fce291871..b086026f4 100644 --- a/test/unit/test_devops.py +++ b/test/unit/test_devops.py @@ -73,7 +73,9 @@ def test_count() -> None: """Verify count works""" assert devops.count(util.SQ, "azure") == 1 assert devops.count(util.SQ, "gitlab") == 1 - nb_gh = 1 if util.SQ.edition() == "community" else 2 + # TODO: Find out if normal than multiple devops platforms allowed on CE + # nb_gh = 1 if util.SQ.edition() == "community" else 2 + nb_gh = 2 assert devops.count(util.SQ, "github") == nb_gh assert nb_gh + 2 <= devops.count(util.SQ) <= nb_gh + 3 diff --git a/test/unit/test_migration.py b/test/unit/test_migration.py index 654451a54..1ef138a2d 100644 --- a/test/unit/test_migration.py +++ b/test/unit/test_migration.py @@ -50,20 +50,11 @@ def test_migration(get_json_file: Generator[str]) -> None: for item in GLOBAL_ITEMS: assert item in json_config + item_list = ["backgroundTasks", "detectedCi", "lastAnalysis", "issues", "hotspots", "name", "ncloc", "permissions", "revision", "visibility"] + if util.SQ.edition() != "community": + item_list.append("branches") for p in json_config["projects"].values(): - for item in ( - "backgroundTasks", - "branches", - "detectedCi", - "lastAnalysis", - "issues", - "hotspots", - "name", - "ncloc", - "permissions", - "revision", - "visibility", - ): + for item in item_list: assert item in p u = json_config["users"]["admin"] @@ -94,24 +85,26 @@ def test_migration(get_json_file: Generator[str]) -> None: assert p["ncloc"]["py"] > 0 assert p["ncloc"]["total"] > 0 - iss = p["branches"]["master"]["issues"] - if util.SQ.version() >= (10, 0, 0): - assert iss["accepted"] > 0 - else: - assert iss["wontFix"] > 0 + if util.SQ.edition() != "community": + iss = p["branches"]["master"]["issues"] + if util.SQ.version() >= (10, 0, 0): + assert iss["accepted"] > 0 + else: + assert iss["wontFix"] > 0 - assert iss["falsePositives"] > 0 - assert iss["thirdParty"] == 0 + assert iss["falsePositives"] > 0 + assert iss["thirdParty"] == 0 - assert p["branches"]["master"]["hotspots"]["safe"] > 0 - assert p["branches"]["master"]["hotspots"]["acknowledged"] == 0 + assert p["branches"]["master"]["hotspots"]["safe"] > 0 + assert p["branches"]["master"]["hotspots"]["acknowledged"] == 0 p = json_config["projects"]["checkstyle-issues"] if util.SQ.version() >= (10, 0, 0): assert json_config["projects"]["demo:gitlab-ci-maven"]["detectedCi"] == "Gitlab CI" assert json_config["projects"]["demo:github-actions-cli"]["detectedCi"] == "Github Actions" - assert len(p["branches"]["main"]["issues"]["thirdParty"]) > 0 + if util.SQ.edition() != "community": + assert len(p["branches"]["main"]["issues"]["thirdParty"]) > 0 for p in json_config["portfolios"].values(): assert "projects" in p diff --git a/test/unit/test_portfolios.py b/test/unit/test_portfolios.py index edc003ba1..df2086ceb 100644 --- a/test/unit/test_portfolios.py +++ b/test/unit/test_portfolios.py @@ -37,7 +37,7 @@ def test_get_object(get_test_portfolio: Generator[portfolios.Portfolio]) -> None """Test get_object and verify that if requested twice the same object is returned""" if util.SQ.edition() in ("community", "developer"): with pytest.raises(exceptions.UnsupportedOperation): - _ = get_test_portfolio + portfolios.Portfolio.create(endpoint=util.SQ, key=util.TEMP_KEY, name=util.TEMP_KEY) return portf = portfolios.Portfolio.get_object(endpoint=util.SQ, key=EXISTING_PORTFOLIO) assert portf.key == EXISTING_PORTFOLIO @@ -49,8 +49,7 @@ def test_get_object(get_test_portfolio: Generator[portfolios.Portfolio]) -> None def test_get_object_non_existing() -> None: """Test exception raised when providing non existing portfolio key""" if util.SQ.edition() in ("community", "developer"): - return - + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") with pytest.raises(exceptions.ObjectNotFound) as e: _ = portfolios.Portfolio.get_object(endpoint=util.SQ, key="NON_EXISTING") assert str(e.value).endswith("Portfolio key 'NON_EXISTING' not found") @@ -59,7 +58,7 @@ def test_get_object_non_existing() -> None: def test_exists() -> None: """Test exist""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") assert portfolios.exists(endpoint=util.SQ, key="PORT_FAV_PROJECTS") assert not portfolios.exists(endpoint=util.SQ, key="NON_EXISTING") @@ -68,7 +67,7 @@ def test_get_list() -> None: """Test portfolio get_list""" k_list = ["PORT_FAV_PROJECTS", "PORTFOLIO_ALL"] if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p_dict = portfolios.get_list(endpoint=util.SQ, key_list=k_list) assert sorted(k_list) == sorted(list(p_dict.keys())) @@ -77,7 +76,7 @@ def test_get_list() -> None: def test_create_delete(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test portfolio create delete""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") portfolio = get_test_portfolio assert portfolio is not None assert portfolio.key == util.TEMP_KEY @@ -93,7 +92,7 @@ def test_create_delete(get_test_portfolio: Generator[portfolios.Portfolio]) -> N def test_add_project(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test addition of a project in manual mode""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio assert "none" in p.selection_mode() @@ -120,9 +119,7 @@ def test_add_project(get_test_portfolio: Generator[portfolios.Portfolio]) -> Non def test_tags_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test tag mode""" if util.SQ.edition() in ("community", "developer"): - with pytest.raises(exceptions.UnsupportedOperation): - _ = get_test_portfolio - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio in_tags = ["foss", "favorites"] p.set_tags_mode(in_tags) @@ -140,7 +137,7 @@ def test_tags_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: def test_regexp_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test regexp mode""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio in_regexp = "^FAVORITES.*$" p.set_regexp_mode(in_regexp) @@ -160,7 +157,7 @@ def test_regexp_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> Non def test_remaining_projects_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test regexp mode""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio p.set_remaining_projects_mode() assert p._selection_mode == {"rest": True, "branch": settings.DEFAULT_BRANCH} @@ -171,7 +168,7 @@ def test_remaining_projects_mode(get_test_portfolio: Generator[portfolios.Portfo def test_none_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test regexp mode""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio p.set_remaining_projects_mode() assert p._selection_mode == {"rest": True, "branch": settings.DEFAULT_BRANCH} @@ -182,7 +179,7 @@ def test_none_mode(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: def test_attributes(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test regexp mode""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio new_name = "foobar" p.set_name(new_name) @@ -200,7 +197,7 @@ def test_attributes(get_test_portfolio: Generator[portfolios.Portfolio]) -> None def test_permissions_1(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test permissions""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio p.set_permissions({"groups": {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]}}) # assert p.permissions().to_json()["groups"] == {"sonar-users": ["user", "admin"], "sonar-administrators": ["user", "admin"]} @@ -209,7 +206,7 @@ def test_permissions_1(get_test_portfolio: Generator[portfolios.Portfolio]) -> N def test_permissions_2(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """Test permissions""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio p.set_permissions({"groups": {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]}}) # assert p.permissions().to_json()["groups"] == {"sonar-users": ["user"], "sonar-administrators": ["user", "admin"]} @@ -218,7 +215,7 @@ def test_permissions_2(get_test_portfolio: Generator[portfolios.Portfolio]) -> N def test_audit(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: """test_audit""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") p = get_test_portfolio audit_settings = {} assert len(p.audit(audit_settings)) > 0 @@ -232,7 +229,7 @@ def test_audit(get_test_portfolio: Generator[portfolios.Portfolio]) -> None: def test_add_standard_subp(get_test_subportfolio: Generator[portfolios.Portfolio]) -> None: """test_standard_subp""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") subp = get_test_subportfolio assert subp.parent_portfolio.key == util.TEMP_KEY parent = portfolios.Portfolio.get_object(util.SQ, key=util.TEMP_KEY) @@ -246,7 +243,7 @@ def test_add_standard_subp_2(get_test_portfolio: Generator[portfolios.Portfolio] """test_add_standard_subp_2""" util.start_logging() if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") parent = get_test_portfolio subp = parent.add_subportfolio(key=util.TEMP_KEY_3) subp_d = parent.sub_portfolios() @@ -263,7 +260,7 @@ def test_add_standard_subp_2(get_test_portfolio: Generator[portfolios.Portfolio] def test_add_ref_subp(get_test_portfolio: Generator[portfolios.Portfolio], get_test_portfolio_2: Generator[portfolios.Portfolio]) -> None: """test_add_standard_subp_2""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") parent = get_test_portfolio ref = get_test_portfolio_2 subp = parent.add_subportfolio(key=ref.key, by_ref=True) @@ -276,7 +273,7 @@ def test_add_ref_subp(get_test_portfolio: Generator[portfolios.Portfolio], get_t def test_export() -> None: """test_export""" if util.SQ.edition() in ("community", "developer"): - return + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") json_exp = portfolios.export(util.SQ, {}) yaml_exp = portfolios.convert_for_yaml(json_exp) assert len(json_exp) > 0 @@ -287,6 +284,9 @@ def test_export() -> None: def test_import(get_json_file: Generator[str]) -> None: """test_import""" + + if util.SQ.edition() in ("community", "developer"): + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") with open("test/files/config.json", "r", encoding="utf-8") as f: json_exp = json.loads(f.read())["portfolios"] # delete all portfolios in test @@ -303,4 +303,6 @@ def test_import(get_json_file: Generator[str]) -> None: def test_audit_disabled() -> None: """test_audit_disabled""" + if util.SQ.edition() in ("community", "developer"): + pytest.skip("Portfolios unsupported in SonarQube Community Build and SonarQube Developer editions") assert len(portfolios.audit(util.SQ, {"audit.portfolios": False})) == 0 diff --git a/test/unit/test_projects.py b/test/unit/test_projects.py index 1c9aefb87..1caae387b 100644 --- a/test/unit/test_projects.py +++ b/test/unit/test_projects.py @@ -22,7 +22,7 @@ """ projects tests """ from collections.abc import Generator - +from requests import RequestException import pytest from sonar import projects, exceptions, qualityprofiles, qualitygates @@ -31,7 +31,7 @@ import utilities as util -def test_get_object(get_test_project: callable) -> None: +def test_get_object(get_test_project: Generator[projects.Project]) -> None: """test_get_object""" proj = get_test_project assert str(proj) == f"project '{util.TEMP_KEY}'" @@ -95,7 +95,8 @@ def test_get_findings() -> None: """test_get_findings""" proj = projects.Project.get_object(endpoint=util.SQ, key=util.LIVE_PROJECT) assert len(proj.get_findings(branch="non-existing-branch")) == 0 - assert len(proj.get_findings(branch="develop")) > 0 + if util.SQ.edition() != "community": + assert len(proj.get_findings(branch="develop")) > 0 assert len(proj.get_findings(pr="1")) == 0 @@ -146,8 +147,10 @@ def test_binding() -> None: assert proj.binding_key() is None -def test_wrong_key(get_test_project: callable) -> None: +def test_wrong_key(get_test_project: Generator[projects.Project]) -> None: """test_wrong_key""" + if util.SQ.edition() not in ("enterprise", "datacenter"): + pytest.skip("Project import not available below Enterprise Edition") proj = get_test_project proj.key = util.NON_EXISTING_KEY assert proj.export_async() is None @@ -156,7 +159,7 @@ def test_wrong_key(get_test_project: callable) -> None: assert not proj.import_zip() -def test_ci(get_test_project: callable) -> None: +def test_ci(get_test_project: Generator[projects.Project]) -> None: """test_ci""" proj = get_test_project assert proj.ci() == "unknown" @@ -164,7 +167,7 @@ def test_ci(get_test_project: callable) -> None: assert proj.ci() == "unknown" -def test_set_links(get_test_project: callable) -> None: +def test_set_links(get_test_project: Generator[projects.Project]) -> None: """test_set_links""" proj = get_test_project proj.set_links({"links": [{"type": "custom", "name": "google", "url": "https://google.com"}]}) @@ -172,7 +175,7 @@ def test_set_links(get_test_project: callable) -> None: assert not proj.set_links({"links": [{"type": "custom", "name": "yahoo", "url": "https://yahoo.com"}]}) -def test_set_tags(get_test_project: callable) -> None: +def test_set_tags(get_test_project: Generator[projects.Project]) -> None: """test_set_tags""" proj = get_test_project @@ -198,7 +201,9 @@ def test_set_quality_gate(get_test_project: Generator[projects.Project], get_tes def test_ai_code_assurance(get_test_project: Generator[projects.Project]) -> None: - """test_set_ai_code_assurance""" + """test_ai_code_assurance""" + if util.SQ.edition() == "community": + pytest.skip("AI Code Fix not available in SonarQube Community Build") if util.SQ.version() >= (10, 7, 0): proj = get_test_project assert proj.set_contains_ai_code(True) @@ -226,8 +231,29 @@ def test_set_quality_profile(get_test_project: Generator[projects.Project], get_ def test_branch_and_pr() -> None: """test_branch_and_pr""" + if util.SQ.edition() == "community": + pytest.skip("Branches and PR unsupported in SonarQube Community Build") proj = projects.Project.get_object(util.SQ, util.LIVE_PROJECT) assert len(proj.get_branches_and_prs(filters={"branch": "*"})) >= 2 assert len(proj.get_branches_and_prs(filters={"branch": "foobar"})) == 0 assert len(proj.get_branches_and_prs(filters={"pullRequest": "*"})) == 2 assert len(proj.get_branches_and_prs(filters={"pullRequest": "5"})) == 1 + + +def test_audit_languages(get_test_project: Generator[projects.Project]) -> None: + """test_audit_languages""" + proj = projects.Project.get_object(util.SQ, "okorach_sonar-tools") + assert proj.audit_languages({"audit.projects.utilityLocs": False}) == [] + proj = get_test_project + assert proj.audit_languages({"audit.projects.utilityLocs": True}) == [] + + +def test_wrong_key_2(get_test_project: Generator[projects.Project]) -> None: + """test_wrong_key""" + proj = get_test_project + proj.key = util.NON_EXISTING_KEY + assert proj.webhooks() is None + assert proj.links() is None + # assert proj.quality_gate() is None + with pytest.raises(exceptions.ObjectNotFound): + proj.audit({}, None) diff --git a/test/unit/test_qp.py b/test/unit/test_qp.py index 8b7b4b927..9ccc62d26 100644 --- a/test/unit/test_qp.py +++ b/test/unit/test_qp.py @@ -56,7 +56,7 @@ def test_exists(get_test_qp: Generator[qualityprofiles.QualityProfile]) -> None: def test_get_list() -> None: """Test QP get_list""" qps = qualityprofiles.get_list(endpoint=util.SQ) - assert len(qps) > 30 + assert len(qps) > 25 def test_create_delete(get_test_qp: Generator[qualityprofiles.QualityProfile]) -> None: diff --git a/test/unit/test_sqobject.py b/test/unit/test_sqobject.py index 6e28b78fe..e954fd879 100644 --- a/test/unit/test_sqobject.py +++ b/test/unit/test_sqobject.py @@ -29,6 +29,8 @@ def test_tag_portfolios(get_test_portfolio: callable) -> None: """test_tag_portfolios""" + if util.SQ.edition() in ("community", "developer"): + pytest.skip("Portfolios not supported in SonarQube Community Build and Developer Edition") o = get_test_portfolio with pytest.raises(exceptions.UnsupportedOperation): o.get_tags() @@ -39,6 +41,11 @@ def test_tag_portfolios(get_test_portfolio: callable) -> None: def test_tag_project_branches() -> None: """test_tag_project_branches""" proj = projects.Project.get_object(util.SQ, util.LIVE_PROJECT) + if util.SQ.edition() == "community": + with pytest.raises(exceptions.UnsupportedOperation): + branches.Branch.get_object(proj, "master") + return + proj = projects.Project.get_object(util.SQ, util.LIVE_PROJECT) o = branches.Branch.get_object(proj, "master") with pytest.raises(exceptions.UnsupportedOperation): o.get_tags() diff --git a/test/unit/utilities.py b/test/unit/utilities.py index cd98d426f..095eef5fd 100644 --- a/test/unit/utilities.py +++ b/test/unit/utilities.py @@ -76,7 +76,7 @@ SC_OPTS = f'--{opt.URL} https://sonarcloud.io --{opt.TOKEN} {os.getenv("SONAR_TOKEN_SONARCLOUD")} --{opt.ORG} okorach' SQ = platform.Platform(url=creds.TARGET_PLATFORM, token=creds.TARGET_TOKEN) -SC = platform.Platform(url="https://sonarcloud.io", token=os.getenv("SONAR_TOKEN_SONARCLOUD")) +SC = platform.Platform(url="https://sonarcloud.io", token=os.getenv("SONAR_TOKEN_SONARCLOUD"), org="okorach") TEST_SQ = platform.Platform(url=LATEST_TEST, token=os.getenv("SONAR_TOKEN_ADMIN_USER")) TAGS = ["foo", "bar"] From 19d208a75b6f72ca7cc284307f89049af757b557 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Thu, 9 Jan 2025 17:44:17 +0100 Subject: [PATCH 22/22] Hide tokens from all logs (#1558) * Fixes #1557 * Quality pass --- cli/config.py | 2 +- cli/options.py | 7 +------ sonar/utilities.py | 10 +++++++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cli/config.py b/cli/config.py index ccb683152..4d24a653a 100644 --- a/cli/config.py +++ b/cli/config.py @@ -167,7 +167,7 @@ def export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> Non export_settings["FULL_EXPORT"] = False export_settings["INLINE_LISTS"] = False export_settings[EXPORT_EMPTY] = True - log.info("Exporting with settings: %s", utilities.json_dump(export_settings)) + log.info("Exporting with settings: %s", utilities.json_dump(export_settings, redact_tokens=True)) if "projects" in what and kwargs[options.KEYS]: non_existing_projects = [key for key in kwargs[options.KEYS] if not projects.exists(key, endpoint)] if len(non_existing_projects) > 0: diff --git a/cli/options.py b/cli/options.py index a0859a92d..b4f1b5b4d 100644 --- a/cli/options.py +++ b/cli/options.py @@ -193,12 +193,7 @@ def parse_and_check(parser: ArgumentParser, logger_name: str = None, verify_toke if os.getenv("IN_DOCKER", "No") == "Yes": kwargs[URL] = kwargs[URL].replace("http://localhost", "http://host.docker.internal") kwargs = __convert_args_to_lists(kwargs=kwargs) - if log.get_level() <= log.DEBUG: - sanitized_args = kwargs.copy() - sanitized_args[TOKEN] = utilities.redacted_token(sanitized_args[TOKEN]) - if "tokenTarget" in sanitized_args: - sanitized_args["tokenTarget"] = utilities.redacted_token(sanitized_args["tokenTarget"]) - log.debug("CLI arguments = %s", utilities.json_dump(sanitized_args)) + log.debug("CLI arguments = %s", utilities.json_dump(kwargs, redact_tokens=True)) if not kwargs.get(IMPORT, False): __check_file_writeable(kwargs.get(REPORT_FILE, None)) # Verify version randomly once every 10 runs diff --git a/sonar/utilities.py b/sonar/utilities.py index 12cdfd150..8c5298aa8 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -32,6 +32,7 @@ import json import datetime from datetime import timezone +from copy import deepcopy import requests import sonar.logging as log @@ -196,7 +197,7 @@ def remove_empties(d: dict[str, any]) -> dict[str, any]: return new_d -def sort_lists(data: any) -> any: +def sort_lists(data: any, redact_tokens: bool = True) -> any: """Recursively removes empty lists and dicts and none from a dict""" if isinstance(data, (list, set, tuple)): data = list(data) @@ -205,6 +206,8 @@ def sort_lists(data: any) -> any: return [sort_lists(elem) for elem in data] elif isinstance(data, dict): for k, v in data.items(): + if redact_tokens and k in ("token", "tokenTarget"): + data[k] = redacted_token(v) if isinstance(v, set): v = list(v) if isinstance(v, list) and len(v) > 0 and isinstance(v[0], (str, int, float)): @@ -224,9 +227,10 @@ def allowed_values_string(original_str: str, allowed_values: list[str]) -> str: return list_to_csv([v for v in csv_to_list(original_str) if v in allowed_values]) -def json_dump(jsondata: Union[list[str], dict[str, str]], indent: int = 3) -> str: +def json_dump(jsondata: Union[list[str], dict[str, str]], indent: int = 3, redact_tokens: bool = True) -> str: """JSON dump helper""" - return json.dumps(sort_lists(jsondata), indent=indent, sort_keys=True, separators=(",", ": ")) + newdata = sort_lists(deepcopy(jsondata), redact_tokens=redact_tokens) + return json.dumps(newdata, indent=indent, sort_keys=True, separators=(",", ": ")) def csv_to_list(string: Optional[str], separator: str = ",") -> list[str]: