上次"移除 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>
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
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
|