From 65c71a57afda84209a352e40ab84f82787d41cda Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 07:09:33 +0000 Subject: [PATCH 1/5] Add OSI-approved license validator for HACS action Add a license validation check that verifies the repository has an OSI-approved open source license, using the SPDX identifier reported by the GitHub API. The set of OSI-approved licenses is fetched on each invocation from the SPDX license list (spdx/license-list-data), pinned to the v3.28.0 release commit, filtering on isOsiApproved. Deprecated SPDX IDs (such as GPL-3.0) are kept since GitHub still reports them for many repositories. The pin carries a renovate annotation comment so a custom manager can keep it updated later. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9QQsoXZefeSfniDY9utsk --- custom_components/hacs/validate/license.py | 54 +++++ tests/action/test_hacs_action_integration.py | 2 +- .../json/licenses.json | 228 ++++++++++++++++++ .../bad_documentation.log | 3 +- .../bad_issue_tracker.log | 3 +- .../no_releases.log | 3 +- .../releases_without_assets.log | 3 +- .../valid_manifest.log | 3 +- ...-action-integration-bad-documentation.json | 3 +- ...-action-integration-bad-issue-tracker.json | 3 +- ...t-hacs-action-integration-no-releases.json | 3 +- ...n-integration-releases-without-assets.json | 3 +- ...acs-action-integration-valid-manifest.json | 3 +- ...icensetest-repository-missing-spdx-id.json | 10 + ...est_licensetest-repository-no-license.json | 9 + ...icensetest-repository-non-osi-license.json | 10 + ...itory-osi-approved-license-apache-2-0.json | 10 + ...pository-osi-approved-license-gpl-3-0.json | 10 + ...t-repository-osi-approved-license-mit.json | 10 + ...etest-repository-unrecognized-license.json | 10 + ...etest-spdx-license-list-fetch-failure.json | 10 + tests/validate/test_license.py | 94 ++++++++ 22 files changed, 476 insertions(+), 11 deletions(-) create mode 100644 custom_components/hacs/validate/license.py create mode 100644 tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-no-license.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-non-osi-license.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-gpl-3-0.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-mit.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-spdx-license-list-fetch-failure.json create mode 100644 tests/validate/test_license.py diff --git a/custom_components/hacs/validate/license.py b/custom_components/hacs/validate/license.py new file mode 100644 index 00000000000..2e325d0743a --- /dev/null +++ b/custom_components/hacs/validate/license.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..utils.json import json_loads +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + +# renovate: datasource=github-tags depName=spdx/license-list-data +SPDX_LICENSE_LIST_COMMIT = "c4a7237ec8f4654e867546f9f409749300f1bf4c" # v3.28.0 + +SPDX_LICENSE_LIST_URL = ( + "https://raw.githubusercontent.com/spdx/license-list-data/" + f"{SPDX_LICENSE_LIST_COMMIT}/json/licenses.json" +) + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-license" + allow_fork = False + + async def async_validate(self) -> None: + """Validate the repository.""" + if (license_info := self.repository.repository_object.attributes.get("license")) is None: + raise ValidationException("The repository has no license") + + result = await self.hacs.async_download_file(SPDX_LICENSE_LIST_URL, handle_rate_limit=True) + if result is None: + raise ValidationException("Could not fetch the SPDX license list") + + osi_approved = { + entry["licenseId"] + for entry in json_loads(result)["licenses"] + if entry.get("isOsiApproved") + } + + spdx_id = license_info.get("spdx_id") + if not spdx_id or spdx_id == "NOASSERTION" or spdx_id not in osi_approved: + raise ValidationException( + f"The repository does not have an OSI-approved license (detected: '{spdx_id}')" + ) + + self.repository.logger.debug( + "The repository has an OSI-approved license: %s", license_info.get("name") + ) diff --git a/tests/action/test_hacs_action_integration.py b/tests/action/test_hacs_action_integration.py index 3ab9d6328a7..355c7496717 100644 --- a/tests/action/test_hacs_action_integration.py +++ b/tests/action/test_hacs_action_integration.py @@ -110,7 +110,7 @@ async def test_hacs_action_integration( await preflight() assert ( - "All (8) checks passed" if test_case["succeed"] else "1/8 checks failed") in caplog.text + "All (9) checks passed" if test_case["succeed"] else "1/9 checks failed") in caplog.text splitlines = [f"<{line.rsplit(' <')[1]}" for line in caplog.text.split( "\n") if " <" in line] diff --git a/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json b/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json new file mode 100644 index 00000000000..cc2e1eca474 --- /dev/null +++ b/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json @@ -0,0 +1,228 @@ +{ + "licenseListVersion": "3.28.0", + "licenses": [ + { + "reference": "https://spdx.org/licenses/AGPL-3.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/AGPL-3.0.json", + "referenceNumber": 90, + "name": "GNU Affero General Public License v3.0", + "licenseId": "AGPL-3.0", + "seeAlso": [ + "https://www.gnu.org/licenses/agpl.txt", + "https://opensource.org/licenses/AGPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/Apache-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Apache-2.0.json", + "referenceNumber": 102, + "name": "Apache License 2.0", + "licenseId": "Apache-2.0", + "seeAlso": [ + "https://www.apache.org/licenses/LICENSE-2.0", + "https://opensource.org/licenses/Apache-2.0", + "https://opensource.org/license/apache-2-0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/BSD-2-Clause.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause.json", + "referenceNumber": 431, + "name": "BSD 2-Clause \"Simplified\" License", + "licenseId": "BSD-2-Clause", + "seeAlso": [ + "https://opensource.org/licenses/BSD-2-Clause" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/BSD-3-Clause.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause.json", + "referenceNumber": 290, + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "licenseId": "BSD-3-Clause", + "seeAlso": [ + "https://opensource.org/licenses/BSD-3-Clause", + "https://www.eclipse.org/org/documents/edl-v10.php" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/BSL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/BSL-1.0.json", + "referenceNumber": 591, + "name": "Boost Software License 1.0", + "licenseId": "BSL-1.0", + "seeAlso": [ + "http://www.boost.org/LICENSE_1_0.txt", + "https://opensource.org/licenses/BSL-1.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/CC-BY-4.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CC-BY-4.0.json", + "referenceNumber": 640, + "name": "Creative Commons Attribution 4.0 International", + "licenseId": "CC-BY-4.0", + "seeAlso": [ + "https://creativecommons.org/licenses/by/4.0/legalcode" + ], + "isOsiApproved": false, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/CC-BY-NC-4.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-4.0.json", + "referenceNumber": 417, + "name": "Creative Commons Attribution Non Commercial 4.0 International", + "licenseId": "CC-BY-NC-4.0", + "seeAlso": [ + "https://creativecommons.org/licenses/by-nc/4.0/legalcode" + ], + "isOsiApproved": false, + "isFsfLibre": false + }, + { + "reference": "https://spdx.org/licenses/CC0-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CC0-1.0.json", + "referenceNumber": 195, + "name": "Creative Commons Zero v1.0 Universal", + "licenseId": "CC0-1.0", + "seeAlso": [ + "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + ], + "isOsiApproved": false, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/EPL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/EPL-2.0.json", + "referenceNumber": 520, + "name": "Eclipse Public License 2.0", + "licenseId": "EPL-2.0", + "seeAlso": [ + "https://www.eclipse.org/legal/epl-2.0", + "https://www.opensource.org/licenses/EPL-2.0", + "https://www.eclipse.org/legal/epl-v20.html", + "https://projects.eclipse.org/license/epl-2.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0.json", + "referenceNumber": 319, + "name": "GNU General Public License v2.0 only", + "licenseId": "GPL-2.0", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", + "https://opensource.org/licenses/GPL-2.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0.json", + "referenceNumber": 164, + "name": "GNU General Public License v3.0 only", + "licenseId": "GPL-3.0", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-3.0-standalone.html", + "https://opensource.org/licenses/GPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.1.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.1.json", + "referenceNumber": 275, + "name": "GNU Lesser General Public License v2.1 only", + "licenseId": "LGPL-2.1", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", + "https://opensource.org/licenses/LGPL-2.1" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/MIT.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MIT.json", + "referenceNumber": 114, + "name": "MIT License", + "licenseId": "MIT", + "seeAlso": [ + "https://opensource.org/license/mit/", + "http://opensource.org/licenses/MIT" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/MPL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MPL-2.0.json", + "referenceNumber": 420, + "name": "Mozilla Public License 2.0", + "licenseId": "MPL-2.0", + "seeAlso": [ + "https://www.mozilla.org/MPL/2.0/", + "https://opensource.org/licenses/MPL-2.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/Unlicense.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Unlicense.json", + "referenceNumber": 141, + "name": "The Unlicense", + "licenseId": "Unlicense", + "seeAlso": [ + "https://unlicense.org/" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/WTFPL.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/WTFPL.json", + "referenceNumber": 455, + "name": "Do What The F*ck You Want To Public License", + "licenseId": "WTFPL", + "seeAlso": [ + "http://www.wtfpl.net/about/", + "http://sam.zoy.org/wtfpl/COPYING" + ], + "isOsiApproved": false, + "isFsfLibre": true + } + ], + "releaseDate": "2026-02-20T00:00:00Z" +} diff --git a/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log b/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log index cf1bdadbd25..e20a2b2732b 100644 --- a/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log +++ b/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log @@ -10,8 +10,9 @@ completed failed: invalid url for dictionary value @ data['documentation']. Got None (More info: https://hacs.xyz/docs/publish/include#check-manifest ) completed + completed completed - 1/8 checks failed + 1/9 checks failed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log b/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log index 982759ef7fb..e2a3458a234 100644 --- a/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log +++ b/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log @@ -10,8 +10,9 @@ completed failed: invalid url for dictionary value @ data['issue_tracker']. Got None (More info: https://hacs.xyz/docs/publish/include#check-manifest ) completed + completed completed - 1/8 checks failed + 1/9 checks failed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/no_releases.log b/tests/snapshots/action/test_hacs_action_integration/no_releases.log index 3da59bf0192..c65f466cb27 100644 --- a/tests/snapshots/action/test_hacs_action_integration/no_releases.log +++ b/tests/snapshots/action/test_hacs_action_integration/no_releases.log @@ -10,8 +10,9 @@ completed completed completed + completed completed - All (8) checks passed + All (9) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log b/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log index 0d9730ccee5..2ed0b2175f7 100644 --- a/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log +++ b/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log @@ -10,8 +10,9 @@ completed completed completed + completed completed - All (8) checks passed + All (9) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log b/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log index f074c070bb7..3647ca91f23 100644 --- a/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log +++ b/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log @@ -10,8 +10,9 @@ completed completed completed + completed completed - All (8) checks passed + All (9) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json index caf277160c2..07d7a370840 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json @@ -7,6 +7,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, - "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 + "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json index 24745ef7f08..670fbe87922 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json @@ -7,6 +7,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, - "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 + "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json index 4cbc668ecb2..4534dda20e4 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json @@ -7,6 +7,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, - "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 + "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json index 79429850924..996578b43f2 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json @@ -7,6 +7,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, - "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 + "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json index 84542d92a4d..ba56c30854d 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json @@ -7,6 +7,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, - "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 + "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json new file mode 100644 index 00000000000..1b94cf10bc4 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_missing_spdx_id": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-no-license.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-no-license.json new file mode 100644 index 00000000000..1cd33df0173 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-no-license.json @@ -0,0 +1,9 @@ +{ + "tests/validate/test_license.py::test_repository_no_license": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-non-osi-license.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-non-osi-license.json new file mode 100644 index 00000000000..fd513c1e859 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-non-osi-license.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_non_osi_license": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json new file mode 100644 index 00000000000..c2ba855bba6 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_osi_approved_license[Apache-2.0]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-gpl-3-0.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-gpl-3-0.json new file mode 100644 index 00000000000..7eaab3e9b73 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-gpl-3-0.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_osi_approved_license[GPL-3.0]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-mit.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-mit.json new file mode 100644 index 00000000000..770f400ccc6 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-mit.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_osi_approved_license[MIT]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json new file mode 100644 index 00000000000..5c89231fbf2 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_repository_unrecognized_license": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-spdx-license-list-fetch-failure.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-spdx-license-list-fetch-failure.json new file mode 100644 index 00000000000..e5d84100147 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-spdx-license-list-fetch-failure.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_license.py::test_spdx_license_list_fetch_failure": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + } +} \ No newline at end of file diff --git a/tests/validate/test_license.py b/tests/validate/test_license.py new file mode 100644 index 00000000000..f173592ecf9 --- /dev/null +++ b/tests/validate/test_license.py @@ -0,0 +1,94 @@ +import json +from unittest.mock import MagicMock + +import pytest + +from custom_components.hacs.validate.license import SPDX_LICENSE_LIST_URL, Validator + +from tests.common import MockedResponse, ResponseMocker + +SPDX_PAYLOAD = json.dumps( + { + "licenseListVersion": "3.28.0", + "licenses": [ + {"licenseId": "MIT", "isOsiApproved": True}, + {"licenseId": "Apache-2.0", "isOsiApproved": True}, + {"licenseId": "GPL-3.0", "isOsiApproved": True, "isDeprecatedLicenseId": True}, + {"licenseId": "CC0-1.0", "isOsiApproved": False}, + ], + } +) + + +async def test_repository_no_license(repository): + repository.repository_object = MagicMock() + repository.repository_object.attributes = {"license": None} + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_repository_unrecognized_license(repository, response_mocker: ResponseMocker): + response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) + repository.repository_object = MagicMock() + repository.repository_object.attributes = { + "license": {"key": "other", "name": "Other", "spdx_id": "NOASSERTION"}, + } + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_repository_missing_spdx_id(repository, response_mocker: ResponseMocker): + response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) + repository.repository_object = MagicMock() + repository.repository_object.attributes = { + "license": {"key": "other", "name": "Other"}, + } + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_repository_non_osi_license(repository, response_mocker: ResponseMocker): + response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) + repository.repository_object = MagicMock() + repository.repository_object.attributes = { + "license": { + "key": "cc0-1.0", + "name": "Creative Commons Zero v1.0 Universal", + "spdx_id": "CC0-1.0", + }, + } + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_spdx_license_list_fetch_failure(repository, response_mocker: ResponseMocker): + response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(status=500)) + repository.repository_object = MagicMock() + repository.repository_object.attributes = { + "license": {"key": "mit", "name": "MIT License", "spdx_id": "MIT"}, + } + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +@pytest.mark.parametrize("spdx_id", ["MIT", "Apache-2.0", "GPL-3.0"]) +async def test_repository_osi_approved_license( + repository, response_mocker: ResponseMocker, spdx_id +): + response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) + repository.repository_object = MagicMock() + repository.repository_object.attributes = { + "license": { + "key": spdx_id.lower(), + "name": f"License {spdx_id}", + "spdx_id": spdx_id, + }, + } + check = Validator(repository) + await check.execute_validation() + assert not check.failed From 24b71107de3561e759fb06096032234de6cd431f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 07:12:39 +0000 Subject: [PATCH 2/5] Trim SPDX license list test fixture to minimum Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9QQsoXZefeSfniDY9utsk --- .../json/licenses.json | 218 +----------------- 1 file changed, 7 insertions(+), 211 deletions(-) diff --git a/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json b/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json index cc2e1eca474..ce0f40bf24c 100644 --- a/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json +++ b/tests/fixtures/proxy/raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json @@ -2,227 +2,23 @@ "licenseListVersion": "3.28.0", "licenses": [ { - "reference": "https://spdx.org/licenses/AGPL-3.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/AGPL-3.0.json", - "referenceNumber": 90, - "name": "GNU Affero General Public License v3.0", - "licenseId": "AGPL-3.0", - "seeAlso": [ - "https://www.gnu.org/licenses/agpl.txt", - "https://opensource.org/licenses/AGPL-3.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/Apache-2.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Apache-2.0.json", - "referenceNumber": 102, - "name": "Apache License 2.0", - "licenseId": "Apache-2.0", - "seeAlso": [ - "https://www.apache.org/licenses/LICENSE-2.0", - "https://opensource.org/licenses/Apache-2.0", - "https://opensource.org/license/apache-2-0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/BSD-2-Clause.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause.json", - "referenceNumber": 431, - "name": "BSD 2-Clause \"Simplified\" License", - "licenseId": "BSD-2-Clause", - "seeAlso": [ - "https://opensource.org/licenses/BSD-2-Clause" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/BSD-3-Clause.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause.json", - "referenceNumber": 290, - "name": "BSD 3-Clause \"New\" or \"Revised\" License", - "licenseId": "BSD-3-Clause", - "seeAlso": [ - "https://opensource.org/licenses/BSD-3-Clause", - "https://www.eclipse.org/org/documents/edl-v10.php" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/BSL-1.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSL-1.0.json", - "referenceNumber": 591, - "name": "Boost Software License 1.0", - "licenseId": "BSL-1.0", - "seeAlso": [ - "http://www.boost.org/LICENSE_1_0.txt", - "https://opensource.org/licenses/BSL-1.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/CC-BY-4.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-4.0.json", - "referenceNumber": 640, - "name": "Creative Commons Attribution 4.0 International", - "licenseId": "CC-BY-4.0", - "seeAlso": [ - "https://creativecommons.org/licenses/by/4.0/legalcode" - ], - "isOsiApproved": false, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/CC-BY-NC-4.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-4.0.json", - "referenceNumber": 417, - "name": "Creative Commons Attribution Non Commercial 4.0 International", - "licenseId": "CC-BY-NC-4.0", - "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/4.0/legalcode" - ], - "isOsiApproved": false, - "isFsfLibre": false - }, - { - "reference": "https://spdx.org/licenses/CC0-1.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC0-1.0.json", - "referenceNumber": 195, - "name": "Creative Commons Zero v1.0 Universal", "licenseId": "CC0-1.0", - "seeAlso": [ - "https://creativecommons.org/publicdomain/zero/1.0/legalcode" - ], - "isOsiApproved": false, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/EPL-2.0.html", + "name": "Creative Commons Zero v1.0 Universal", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EPL-2.0.json", - "referenceNumber": 520, - "name": "Eclipse Public License 2.0", - "licenseId": "EPL-2.0", - "seeAlso": [ - "https://www.eclipse.org/legal/epl-2.0", - "https://www.opensource.org/licenses/EPL-2.0", - "https://www.eclipse.org/legal/epl-v20.html", - "https://projects.eclipse.org/license/epl-2.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/GPL-2.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0.json", - "referenceNumber": 319, - "name": "GNU General Public License v2.0 only", - "licenseId": "GPL-2.0", - "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", - "https://opensource.org/licenses/GPL-2.0" - ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0.json", - "referenceNumber": 164, - "name": "GNU General Public License v3.0 only", "licenseId": "GPL-3.0", - "seeAlso": [ - "https://www.gnu.org/licenses/gpl-3.0-standalone.html", - "https://opensource.org/licenses/GPL-3.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/LGPL-2.1.html", + "name": "GNU General Public License v3.0 only", "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.1.json", - "referenceNumber": 275, - "name": "GNU Lesser General Public License v2.1 only", - "licenseId": "LGPL-2.1", - "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", - "https://opensource.org/licenses/LGPL-2.1" - ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/MIT.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "referenceNumber": 114, - "name": "MIT License", "licenseId": "MIT", - "seeAlso": [ - "https://opensource.org/license/mit/", - "http://opensource.org/licenses/MIT" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/MPL-2.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MPL-2.0.json", - "referenceNumber": 420, - "name": "Mozilla Public License 2.0", - "licenseId": "MPL-2.0", - "seeAlso": [ - "https://www.mozilla.org/MPL/2.0/", - "https://opensource.org/licenses/MPL-2.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/Unlicense.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Unlicense.json", - "referenceNumber": 141, - "name": "The Unlicense", - "licenseId": "Unlicense", - "seeAlso": [ - "https://unlicense.org/" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/WTFPL.html", + "name": "MIT License", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/WTFPL.json", - "referenceNumber": 455, - "name": "Do What The F*ck You Want To Public License", - "licenseId": "WTFPL", - "seeAlso": [ - "http://www.wtfpl.net/about/", - "http://sam.zoy.org/wtfpl/COPYING" - ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true } ], - "releaseDate": "2026-02-20T00:00:00Z" + "releaseDate": "2025-05-30" } From b0318e67e8f8fb15e0bfcd12aaa64f7dc67c246c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 07:13:41 +0000 Subject: [PATCH 3/5] Use proxy fixture instead of inline SPDX payload in license tests Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9QQsoXZefeSfniDY9utsk --- ...itory-osi-approved-license-apache-2-0.json | 10 ------- tests/validate/test_license.py | 29 ++++--------------- 2 files changed, 5 insertions(+), 34 deletions(-) delete mode 100644 tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json deleted file mode 100644 index c2ba855bba6..00000000000 --- a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-osi-approved-license-apache-2-0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "tests/validate/test_license.py::test_repository_osi_approved_license[Apache-2.0]": { - "https://api.github.com/repos/hacs/integration": 1, - "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, - "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, - "https://api.github.com/repos/hacs/integration/git/trees/main": 1, - "https://api.github.com/repos/hacs/integration/releases": 1, - "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 - } -} \ No newline at end of file diff --git a/tests/validate/test_license.py b/tests/validate/test_license.py index f173592ecf9..0d90a5ddc3d 100644 --- a/tests/validate/test_license.py +++ b/tests/validate/test_license.py @@ -1,4 +1,3 @@ -import json from unittest.mock import MagicMock import pytest @@ -7,18 +6,6 @@ from tests.common import MockedResponse, ResponseMocker -SPDX_PAYLOAD = json.dumps( - { - "licenseListVersion": "3.28.0", - "licenses": [ - {"licenseId": "MIT", "isOsiApproved": True}, - {"licenseId": "Apache-2.0", "isOsiApproved": True}, - {"licenseId": "GPL-3.0", "isOsiApproved": True, "isDeprecatedLicenseId": True}, - {"licenseId": "CC0-1.0", "isOsiApproved": False}, - ], - } -) - async def test_repository_no_license(repository): repository.repository_object = MagicMock() @@ -28,8 +15,7 @@ async def test_repository_no_license(repository): assert check.failed -async def test_repository_unrecognized_license(repository, response_mocker: ResponseMocker): - response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) +async def test_repository_unrecognized_license(repository): repository.repository_object = MagicMock() repository.repository_object.attributes = { "license": {"key": "other", "name": "Other", "spdx_id": "NOASSERTION"}, @@ -39,8 +25,7 @@ async def test_repository_unrecognized_license(repository, response_mocker: Resp assert check.failed -async def test_repository_missing_spdx_id(repository, response_mocker: ResponseMocker): - response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) +async def test_repository_missing_spdx_id(repository): repository.repository_object = MagicMock() repository.repository_object.attributes = { "license": {"key": "other", "name": "Other"}, @@ -50,8 +35,7 @@ async def test_repository_missing_spdx_id(repository, response_mocker: ResponseM assert check.failed -async def test_repository_non_osi_license(repository, response_mocker: ResponseMocker): - response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) +async def test_repository_non_osi_license(repository): repository.repository_object = MagicMock() repository.repository_object.attributes = { "license": { @@ -76,11 +60,8 @@ async def test_spdx_license_list_fetch_failure(repository, response_mocker: Resp assert check.failed -@pytest.mark.parametrize("spdx_id", ["MIT", "Apache-2.0", "GPL-3.0"]) -async def test_repository_osi_approved_license( - repository, response_mocker: ResponseMocker, spdx_id -): - response_mocker.add(SPDX_LICENSE_LIST_URL, MockedResponse(content=SPDX_PAYLOAD)) +@pytest.mark.parametrize("spdx_id", ["MIT", "GPL-3.0"]) +async def test_repository_osi_approved_license(repository, spdx_id): repository.repository_object = MagicMock() repository.repository_object.attributes = { "license": { From 2a0d92e3817653381399b2e0881e6754c0777e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 3 Jul 2026 10:01:22 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- custom_components/hacs/validate/license.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/hacs/validate/license.py b/custom_components/hacs/validate/license.py index 2e325d0743a..3f51c6af884 100644 --- a/custom_components/hacs/validate/license.py +++ b/custom_components/hacs/validate/license.py @@ -37,14 +37,25 @@ async def async_validate(self) -> None: if result is None: raise ValidationException("Could not fetch the SPDX license list") + try: + licenses = json_loads(result).get("licenses", []) + except Exception as err: + raise ValidationException("Could not parse the SPDX license list") from err + osi_approved = { - entry["licenseId"] - for entry in json_loads(result)["licenses"] - if entry.get("isOsiApproved") + entry.get("licenseId") + for entry in licenses + if entry.get("isOsiApproved") and entry.get("licenseId") } spdx_id = license_info.get("spdx_id") - if not spdx_id or spdx_id == "NOASSERTION" or spdx_id not in osi_approved: + if not spdx_id: + raise ValidationException("The repository license is missing an SPDX ID") + if spdx_id == "NOASSERTION": + raise ValidationException( + "The repository license could not be identified (SPDX: NOASSERTION)" + ) + if spdx_id not in osi_approved: raise ValidationException( f"The repository does not have an OSI-approved license (detected: '{spdx_id}')" ) From 2ad6253ed41a06ac0d4ed165a1bac9bdb6def38e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 08:12:23 +0000 Subject: [PATCH 5/5] Short-circuit license validation before SPDX list download Fail immediately when the license has no usable SPDX ID (missing or NOASSERTION) instead of downloading the SPDX license list first, since the check can never pass in those cases. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9QQsoXZefeSfniDY9utsk --- custom_components/hacs/validate/license.py | 15 ++++++++------- ...st_licensetest-repository-missing-spdx-id.json | 3 +-- ...censetest-repository-unrecognized-license.json | 3 +-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/custom_components/hacs/validate/license.py b/custom_components/hacs/validate/license.py index 3f51c6af884..2e6ec6a1261 100644 --- a/custom_components/hacs/validate/license.py +++ b/custom_components/hacs/validate/license.py @@ -33,6 +33,14 @@ async def async_validate(self) -> None: if (license_info := self.repository.repository_object.attributes.get("license")) is None: raise ValidationException("The repository has no license") + spdx_id = license_info.get("spdx_id") + if not spdx_id: + raise ValidationException("The repository license is missing an SPDX ID") + if spdx_id == "NOASSERTION": + raise ValidationException( + "The repository license could not be identified (SPDX: NOASSERTION)" + ) + result = await self.hacs.async_download_file(SPDX_LICENSE_LIST_URL, handle_rate_limit=True) if result is None: raise ValidationException("Could not fetch the SPDX license list") @@ -48,13 +56,6 @@ async def async_validate(self) -> None: if entry.get("isOsiApproved") and entry.get("licenseId") } - spdx_id = license_info.get("spdx_id") - if not spdx_id: - raise ValidationException("The repository license is missing an SPDX ID") - if spdx_id == "NOASSERTION": - raise ValidationException( - "The repository license could not be identified (SPDX: NOASSERTION)" - ) if spdx_id not in osi_approved: raise ValidationException( f"The repository does not have an OSI-approved license (detected: '{spdx_id}')" diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json index 1b94cf10bc4..231cd2223b1 100644 --- a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-missing-spdx-id.json @@ -4,7 +4,6 @@ "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, "https://api.github.com/repos/hacs/integration/git/trees/main": 1, - "https://api.github.com/repos/hacs/integration/releases": 1, - "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + "https://api.github.com/repos/hacs/integration/releases": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json index 5c89231fbf2..f24cf187a52 100644 --- a/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json +++ b/tests/snapshots/api-usage/tests/validate/test_licensetest-repository-unrecognized-license.json @@ -4,7 +4,6 @@ "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, "https://api.github.com/repos/hacs/integration/git/trees/main": 1, - "https://api.github.com/repos/hacs/integration/releases": 1, - "https://raw.githubusercontent.com/spdx/license-list-data/c4a7237ec8f4654e867546f9f409749300f1bf4c/json/licenses.json": 1 + "https://api.github.com/repos/hacs/integration/releases": 1 } } \ No newline at end of file