"""Tests covering the security fixes made in this session.
Each test class is named after the fix it verifies:
- TestB104ConfigHttpsValidator : 0.0.0.0 no longer exempt from HTTPS check
- TestBuildCommentBodyRefactor : list+join rewrite preserves all output
- TestDiscoverDropdownRefactor : list+join rewrite produces correct HTML
- TestRequestsTimeouts : all HTTP calls in hf.py carry explicit timeouts
"""
import importlib
import json
import re
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# ── B104: AI_EXPLAINER_BASE_URL validator ────────────────────────────────────
class TestB104ConfigHttpsValidator:
"""AI_EXPLAINER_BASE_URL must reject http:// for non-localhost hosts.
Previously 0.0.0.0 was in the localhost allowlist, which bandit (B104)
flagged as 'binding to all interfaces'. The fix removes 0.0.0.0 from the
allowlist so that http://0.0.0.0:... is treated as a remote URL and
rejected.
"""
def _make_settings(self, url: str):
"""Import Settings fresh so environment doesn't pollute the test."""
import importlib
import sentinel.config as cfg_mod
importlib.reload(cfg_mod)
from pydantic_settings import BaseSettings
return cfg_mod.Settings(
AI_EXPLAINER_BASE_URL=url,
DATABASE_URL="sqlite+aiosqlite:////tmp/test.db",
DATA_DIR="/tmp",
)
def test_localhost_http_allowed(self):
s = self._make_settings("http://localhost:11434")
assert s.AI_EXPLAINER_BASE_URL == "http://localhost:11434"
def test_loopback_ip_http_allowed(self):
s = self._make_settings("http://127.0.0.1:11434")
assert s.AI_EXPLAINER_BASE_URL == "http://127.0.0.1:11434"
def test_ipv6_loopback_http_allowed(self):
s = self._make_settings("http://[::1]:11434")
assert s.AI_EXPLAINER_BASE_URL == "http://[::1]:11434"
def test_https_remote_allowed(self):
s = self._make_settings("https://ollama.example.com")
assert s.AI_EXPLAINER_BASE_URL == "https://ollama.example.com"
def test_http_remote_rejected(self):
from pydantic import ValidationError
with pytest.raises((ValidationError, ValueError)):
self._make_settings("http://ollama.example.com")
def test_bind_all_0000_now_rejected(self):
"""0.0.0.0 is NOT a loopback address — it must be rejected for http://."""
from pydantic import ValidationError
with pytest.raises((ValidationError, ValueError)):
self._make_settings("http://0.0.0.0:11434")
# ── _build_comment_body: list+join refactor correctness ─────────────────────
class TestBuildCommentBodyRefactor:
"""Verify the list+join rewrite produces the same output as the old body+=.
The performance fix changed string concatenation inside the per-finding
loop to list accumulation + ''.join() at the end. All existing content
invariants must still hold.
"""
def _finding(self, **kwargs) -> dict:
defaults = dict(
file="app.py", line=10, confidence="likely",
message="Shell injection found", remediation="Use subprocess.run",
)
defaults.update(kwargs)
return defaults
def _body(self, findings):
from core.hf import _build_comment_body
return _build_comment_body(findings)
def test_returns_string(self):
assert isinstance(self._body([self._finding()]), str)
def test_header_present(self):
assert "security scan findings" in self._body([self._finding()])
def test_details_block_present(self):
body = self._body([self._finding()])
assert "