Files
sentinel-home-ai/fam-core/tests/test_auth.py
ericwyuan 09708bd9a4 fix(fam-core): 修复 /api/status 和 /api/ss/status 500(移除 Webhook 重构遗留死引用)
上次"移除 SS Webhook 可选路径"重构(cf3d145)删掉了 MotionNotifier 的
camera_name_to_id/_ss_name_to_id/_camera_loaded 三个属性初始化,但
status() 方法忘记同步删除对它们的引用,导致任何调用 status() 的地方
(/api/status、/api/ss/status)直接抛 AttributeError 500——这两个端点
自那次重构部署后就一直是坏的,服务状态页无法访问。

顺带修 test_auth.py 里断言 /api/ss/webhook 应该被鉴权白名单放行的过时
测试——这个端点本身已经在同一次重构里被彻底删除(motion_bp.py 不再
注册该路由),白名单移除是对的,是测试没跟上。

已在 NAS 上部署验证:登录后 /api/status 恢复正常返回。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:29:09 +08:00

107 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import time
import pytest
from fam_core import auth
@pytest.fixture(autouse=True)
def _clean_env_and_sessions(monkeypatch):
"""每个用例前清掉环境变量和进程内 session 表,用例之间不互相污染。"""
monkeypatch.delenv('FAM_AUTH_USER', raising=False)
monkeypatch.delenv('FAM_AUTH_PASS', raising=False)
auth._sessions.clear()
auth._warned_unconfigured = False
yield
auth._sessions.clear()
def test_check_credential_fails_closed_when_unconfigured():
"""核心诉求: .env 没配置 FAM_AUTH_USER/FAM_AUTH_PASS 时必须拒绝所有登录,
不能退回任何硬编码默认账号密码(这两个变量跟 NAS SSH 密码是同一个值,公网
入口不能有"没配置就用已知密码兜底"这种行为)。"""
assert auth._check_credential('ericwyuan', 'iLoveJava5') is False
assert auth._check_credential('anything', 'anything') is False
assert auth._check_credential('', '') is False
def test_check_credential_succeeds_with_matching_configured_values(monkeypatch):
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
monkeypatch.setenv('FAM_AUTH_PASS', 'testpass')
assert auth._check_credential('testuser', 'testpass') is True
def test_check_credential_rejects_wrong_password_when_configured(monkeypatch):
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
monkeypatch.setenv('FAM_AUTH_PASS', 'testpass')
assert auth._check_credential('testuser', 'wrongpass') is False
assert auth._check_credential('wronguser', 'testpass') is False
def test_check_credential_fails_closed_when_only_one_var_set(monkeypatch):
"""只配了一半(比如账号忘配密码)也要 fail closed不能退化成"密码随便""""
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
assert auth._check_credential('testuser', '') is False
assert auth._check_credential('testuser', 'anything') is False
def test_session_ttl_is_two_hours():
assert auth._SESSION_TTL == 2 * 3600
def test_is_authed_true_within_ttl():
tok = 'sometoken'
auth._sessions[tok] = time.time() + 3600
class _FakeRequest:
cookies = {'fam_session': tok}
monkeypatch_request = auth.request
try:
auth.request = _FakeRequest()
assert auth.is_authed() is True
finally:
auth.request = monkeypatch_request
def test_is_authed_false_after_expiry():
tok = 'expiredtoken'
auth._sessions[tok] = time.time() - 1 # 已过期
class _FakeRequest:
cookies = {'fam_session': tok}
monkeypatch_request = auth.request
try:
auth.request = _FakeRequest()
assert auth.is_authed() is False
# 过期后应该从 session 表里清掉,不留垃圾
assert tok not in auth._sessions
finally:
auth.request = monkeypatch_request
def test_is_authed_false_without_cookie():
class _FakeRequest:
cookies = {}
monkeypatch_request = auth.request
try:
auth.request = _FakeRequest()
assert auth.is_authed() is False
finally:
auth.request = monkeypatch_request
def test_whitelist_exact_paths():
for path in ('/login', '/api/login', '/api/logout', '/api/auth/check',
'/health', '/favicon.ico'):
assert auth._is_whitelisted(path) is True
def test_whitelist_assets_prefix():
assert auth._is_whitelisted('/assets/index-abc123.js') is True
def test_whitelist_rejects_protected_paths():
for path in ('/', '/api/ui/people', '/api/chat/ask', '/api/ss/status'):
assert auth._is_whitelisted(path) is False