From 62e788c7da921e7ac81b6830a5f002fc10b3ee7d Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 28 Nov 2024 17:58:56 +0100
Subject: [PATCH] Add config flow rules to quality_scale hassfest validation
 (#131791)

* Add config flow rules to quality_scale hassfest validation

* Use integration.config_flow property
---
 .../components/mqtt/quality_scale.yaml        |  5 +++-
 script/hassfest/quality_scale.py              | 16 +++++++---
 .../config_entry_unloading.py                 |  5 +++-
 .../quality_scale_validation/config_flow.py   | 24 +++++++++++++++
 .../reauthentication_flow.py                  | 30 +++++++++++++++++++
 .../reconfiguration_flow.py                   | 30 +++++++++++++++++++
 6 files changed, 104 insertions(+), 6 deletions(-)
 create mode 100644 script/hassfest/quality_scale_validation/config_flow.py
 create mode 100644 script/hassfest/quality_scale_validation/reauthentication_flow.py
 create mode 100644 script/hassfest/quality_scale_validation/reconfiguration_flow.py

diff --git a/homeassistant/components/mqtt/quality_scale.yaml b/homeassistant/components/mqtt/quality_scale.yaml
index b3084f67da3..d459f0420f1 100644
--- a/homeassistant/components/mqtt/quality_scale.yaml
+++ b/homeassistant/components/mqtt/quality_scale.yaml
@@ -86,7 +86,10 @@ rules:
     comment: >
       This is not possible because the integrations generates entities
       based on a user supplied config or discovery.
-  reconfiguration-flow: done
+  reconfiguration-flow:
+    status: exempt
+    comment: >
+      This integration is reconfigured via options flow.
   dynamic-devices:
     status: done
     comment: |
diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py
index 980d659b03e..3e8d25c1f32 100644
--- a/script/hassfest/quality_scale.py
+++ b/script/hassfest/quality_scale.py
@@ -12,7 +12,13 @@ from homeassistant.exceptions import HomeAssistantError
 from homeassistant.util.yaml import load_yaml_dict
 
 from .model import Config, Integration, ScaledQualityScaleTiers
-from .quality_scale_validation import RuleValidationProtocol, config_entry_unloading
+from .quality_scale_validation import (
+    RuleValidationProtocol,
+    config_entry_unloading,
+    config_flow,
+    reauthentication_flow,
+    reconfiguration_flow,
+)
 
 QUALITY_SCALE_TIERS = {value.name.lower(): value for value in ScaledQualityScaleTiers}
 
@@ -32,7 +38,7 @@ ALL_RULES = [
     Rule("appropriate-polling", ScaledQualityScaleTiers.BRONZE),
     Rule("brands", ScaledQualityScaleTiers.BRONZE),
     Rule("common-modules", ScaledQualityScaleTiers.BRONZE),
-    Rule("config-flow", ScaledQualityScaleTiers.BRONZE),
+    Rule("config-flow", ScaledQualityScaleTiers.BRONZE, config_flow),
     Rule("config-flow-test-coverage", ScaledQualityScaleTiers.BRONZE),
     Rule("dependency-transparency", ScaledQualityScaleTiers.BRONZE),
     Rule("docs-actions", ScaledQualityScaleTiers.BRONZE),
@@ -57,7 +63,9 @@ ALL_RULES = [
     Rule("integration-owner", ScaledQualityScaleTiers.SILVER),
     Rule("log-when-unavailable", ScaledQualityScaleTiers.SILVER),
     Rule("parallel-updates", ScaledQualityScaleTiers.SILVER),
-    Rule("reauthentication-flow", ScaledQualityScaleTiers.SILVER),
+    Rule(
+        "reauthentication-flow", ScaledQualityScaleTiers.SILVER, reauthentication_flow
+    ),
     Rule("test-coverage", ScaledQualityScaleTiers.SILVER),
     # GOLD: [
     Rule("devices", ScaledQualityScaleTiers.GOLD),
@@ -78,7 +86,7 @@ ALL_RULES = [
     Rule("entity-translations", ScaledQualityScaleTiers.GOLD),
     Rule("exception-translations", ScaledQualityScaleTiers.GOLD),
     Rule("icon-translations", ScaledQualityScaleTiers.GOLD),
-    Rule("reconfiguration-flow", ScaledQualityScaleTiers.GOLD),
+    Rule("reconfiguration-flow", ScaledQualityScaleTiers.GOLD, reconfiguration_flow),
     Rule("repair-issues", ScaledQualityScaleTiers.GOLD),
     Rule("stale-devices", ScaledQualityScaleTiers.GOLD),
     # PLATINUM
diff --git a/script/hassfest/quality_scale_validation/config_entry_unloading.py b/script/hassfest/quality_scale_validation/config_entry_unloading.py
index 42134e0391e..63b0117498e 100644
--- a/script/hassfest/quality_scale_validation/config_entry_unloading.py
+++ b/script/hassfest/quality_scale_validation/config_entry_unloading.py
@@ -1,4 +1,7 @@
-"""Enforce that the integration implements entry unloading."""
+"""Enforce that the integration implements entry unloading.
+
+https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-entry-unloading/
+"""
 
 import ast
 
diff --git a/script/hassfest/quality_scale_validation/config_flow.py b/script/hassfest/quality_scale_validation/config_flow.py
new file mode 100644
index 00000000000..e1361d6550f
--- /dev/null
+++ b/script/hassfest/quality_scale_validation/config_flow.py
@@ -0,0 +1,24 @@
+"""Enforce that the integration implements config flow.
+
+https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-flow/
+"""
+
+from script.hassfest.model import Integration
+
+
+def validate(integration: Integration) -> list[str] | None:
+    """Validate that the integration implements config flow."""
+
+    if not integration.config_flow:
+        return [
+            "Integration does not set config_flow in its manifest "
+            f"homeassistant/components/{integration.domain}/manifest.json",
+        ]
+
+    config_flow_file = integration.path / "config_flow.py"
+    if not config_flow_file.exists():
+        return [
+            "Integration does not implement config flow (is missing config_flow.py)",
+        ]
+
+    return None
diff --git a/script/hassfest/quality_scale_validation/reauthentication_flow.py b/script/hassfest/quality_scale_validation/reauthentication_flow.py
new file mode 100644
index 00000000000..d4bc8ed6e96
--- /dev/null
+++ b/script/hassfest/quality_scale_validation/reauthentication_flow.py
@@ -0,0 +1,30 @@
+"""Enforce that the integration implements reauthentication flow.
+
+https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/reauthentication-flow/
+"""
+
+import ast
+
+from script.hassfest.model import Integration
+
+
+def _has_async_function(module: ast.Module, name: str) -> bool:
+    """Test if the module defines a function."""
+    return any(
+        type(item) is ast.AsyncFunctionDef and item.name == name
+        for item in ast.walk(module)
+    )
+
+
+def validate(integration: Integration) -> list[str] | None:
+    """Validate that the integration has a reauthentication flow."""
+
+    config_flow_file = integration.path / "config_flow.py"
+    config_flow = ast.parse(config_flow_file.read_text())
+
+    if not _has_async_function(config_flow, "async_step_reauth"):
+        return [
+            "Integration does not support a reauthentication flow "
+            f"(is missing `async_step_reauth` in {config_flow_file})"
+        ]
+    return None
diff --git a/script/hassfest/quality_scale_validation/reconfiguration_flow.py b/script/hassfest/quality_scale_validation/reconfiguration_flow.py
new file mode 100644
index 00000000000..94547e95625
--- /dev/null
+++ b/script/hassfest/quality_scale_validation/reconfiguration_flow.py
@@ -0,0 +1,30 @@
+"""Enforce that the integration implements reconfiguration flow.
+
+https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/reconfiguration-flow/
+"""
+
+import ast
+
+from script.hassfest.model import Integration
+
+
+def _has_async_function(module: ast.Module, name: str) -> bool:
+    """Test if the module defines a function."""
+    return any(
+        type(item) is ast.AsyncFunctionDef and item.name == name
+        for item in ast.walk(module)
+    )
+
+
+def validate(integration: Integration) -> list[str] | None:
+    """Validate that the integration has a reconfiguration flow."""
+
+    config_flow_file = integration.path / "config_flow.py"
+    config_flow = ast.parse(config_flow_file.read_text())
+
+    if not _has_async_function(config_flow, "async_step_reconfigure"):
+        return [
+            "Integration does not support a reconfiguration flow "
+            f"(is missing `async_step_reconfigure` in {config_flow_file})"
+        ]
+    return None
-- 
GitLab