Fix cookie-jar pollution; remove dead QR login code; add tests
The reauth flow ran validate_jsessionid against HA's shared aiohttp
session, then update_cookies({"JSESSIONID": ...}) added the new value
with no domain. aiohttp's cookie jar still held the previous
JSESSIONID pinned to smartthingsfind.samsung.com (set via Set-Cookie
during the previous load), and aiohttp prefers the more-specific
domain match — so the *stale* cookie went out, Samsung returned no
_csrf header, and the user saw "Cookie was rejected by SmartThings
Find" even though their cookie was fine.
Two fixes:
* validate_jsessionid now runs in an isolated aiohttp.ClientSession
with its own jar, so the shared HA jar can't shadow the cookie
under test.
* async_setup_entry clear_domain()s the smartthingsfind.samsung.com
cookies before reseating JSESSIONID with response_url, otherwise
the same shadowing breaks the entry reload that follows a
successful UI reauth.
Also remove the QR-code login code (do_login_stage_one / _two,
gen_qr_code_base64, the legacy URL constants and qrcode/base64/
random/string/re/asyncio/io/timedelta imports) — Samsung migrated
account.samsung.com to a SPA-driven IAM/OAuth2 flow months ago, so
the QR scrape no longer works and nothing in the integration
references those helpers anymore. Drops the qrcode/pillow/requests
manifest requirements.
Tests: a minimal conftest stubs the homeassistant.* imports the
integration uses, and four async tests cover validate_jsessionid
including the regression case where a domain-bound stale cookie
sits in the shared jar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7457a8284b
commit
4c26792c37
6 changed files with 287 additions and 227 deletions
158
tests/conftest.py
Normal file
158
tests/conftest.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""Stub the HA imports the integration touches so utils/config_flow can be
|
||||
imported and exercised without a full Home Assistant install.
|
||||
|
||||
Only the bits used by the code under test are faked — anything new the
|
||||
integration starts touching will need to be added here."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
|
||||
def _module(name: str) -> types.ModuleType:
|
||||
if name not in sys.modules:
|
||||
sys.modules[name] = types.ModuleType(name)
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
# homeassistant.core
|
||||
ha = _module("homeassistant")
|
||||
core = _module("homeassistant.core")
|
||||
|
||||
|
||||
class HomeAssistant:
|
||||
def __init__(self) -> None:
|
||||
self.data: dict[str, Any] = {}
|
||||
# Tests assign a real aiohttp.ClientSession here.
|
||||
self.clientsession = None
|
||||
|
||||
|
||||
def callback(func):
|
||||
return func
|
||||
|
||||
|
||||
core.HomeAssistant = HomeAssistant
|
||||
core.callback = callback
|
||||
ha.core = core
|
||||
|
||||
# homeassistant.exceptions
|
||||
exc = _module("homeassistant.exceptions")
|
||||
|
||||
|
||||
class ConfigEntryAuthFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigEntryNotReady(Exception):
|
||||
pass
|
||||
|
||||
|
||||
exc.ConfigEntryAuthFailed = ConfigEntryAuthFailed
|
||||
exc.ConfigEntryNotReady = ConfigEntryNotReady
|
||||
|
||||
# homeassistant.helpers + submodules
|
||||
helpers = _module("homeassistant.helpers")
|
||||
aiohttp_client = _module("homeassistant.helpers.aiohttp_client")
|
||||
|
||||
|
||||
def async_get_clientsession(hass: HomeAssistant):
|
||||
return hass.clientsession
|
||||
|
||||
|
||||
aiohttp_client.async_get_clientsession = async_get_clientsession
|
||||
helpers.aiohttp_client = aiohttp_client
|
||||
|
||||
entity = _module("homeassistant.helpers.entity")
|
||||
|
||||
|
||||
class DeviceInfo(dict):
|
||||
pass
|
||||
|
||||
|
||||
entity.DeviceInfo = DeviceInfo
|
||||
helpers.entity = entity
|
||||
|
||||
device_registry = _module("homeassistant.helpers.device_registry")
|
||||
|
||||
|
||||
class _FakeDeviceRegistry:
|
||||
def async_get_device(self, identifiers):
|
||||
return None
|
||||
|
||||
|
||||
def _async_get(_hass):
|
||||
return _FakeDeviceRegistry()
|
||||
|
||||
|
||||
device_registry.async_get = _async_get
|
||||
helpers.device_registry = device_registry
|
||||
|
||||
typing_module = _module("homeassistant.helpers.typing")
|
||||
typing_module.ConfigType = dict
|
||||
|
||||
# config_entries
|
||||
config_entries = _module("homeassistant.config_entries")
|
||||
|
||||
|
||||
class ConfigEntry:
|
||||
pass
|
||||
|
||||
|
||||
class ConfigFlow:
|
||||
def __init_subclass__(cls, *, domain=None, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
|
||||
class OptionsFlow:
|
||||
pass
|
||||
|
||||
|
||||
class OptionsFlowWithConfigEntry(OptionsFlow):
|
||||
def __init__(self, config_entry):
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(getattr(config_entry, "options", {}) or {})
|
||||
|
||||
|
||||
CONN_CLASS_CLOUD_POLL = "cloud_poll"
|
||||
ConfigFlowResult = dict
|
||||
|
||||
config_entries.ConfigEntry = ConfigEntry
|
||||
config_entries.ConfigFlow = ConfigFlow
|
||||
config_entries.OptionsFlow = OptionsFlow
|
||||
config_entries.OptionsFlowWithConfigEntry = OptionsFlowWithConfigEntry
|
||||
config_entries.CONN_CLASS_CLOUD_POLL = CONN_CLASS_CLOUD_POLL
|
||||
config_entries.ConfigFlowResult = ConfigFlowResult
|
||||
|
||||
# const
|
||||
const = _module("homeassistant.const")
|
||||
|
||||
|
||||
class Platform:
|
||||
DEVICE_TRACKER = "device_tracker"
|
||||
BUTTON = "button"
|
||||
SENSOR = "sensor"
|
||||
|
||||
|
||||
const.Platform = Platform
|
||||
|
||||
# update_coordinator
|
||||
update_coord = _module("homeassistant.helpers.update_coordinator")
|
||||
|
||||
|
||||
class DataUpdateCoordinator:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
update_coord.DataUpdateCoordinator = DataUpdateCoordinator
|
||||
update_coord.UpdateFailed = UpdateFailed
|
||||
helpers.update_coordinator = update_coord
|
||||
Loading…
Add table
Add a link
Reference in a new issue