stage-30: home page (Newsline digest) + PWA + mobile pass
NEW / start page replaces the redirect-to-/cases: - KPI strip (cases, IOCs, +24h, high/critical, ⚡ enforced 24h, ledger total) — clickable, responsive grid (2 cols mobile, 3 mid, 6 desktop). - Recent activity feed: ledger events (enforced/submitted/rejected/failed) + newest case ingests, interleaved newest-first, with severity badges, icons, case links. Sources via lines/news.py. - Feed health sidebar: per-feed count + last ingest time. PWA: - /static/manifest.json declares a standalone install with theme colors. - /static/sw.js — cache-first for static, network-first for HTML/API, with a graceful offline page. Registered from / scope via a dedicated /sw.js route that sets Service-Worker-Allowed: /. - viewport + apple-touch-icon + theme-color meta tags in base.html. Mobile pass on the chrome: - Topbar wraps; nav horizontally scrolls instead of crowding; brand-sub hides. - Tables (cases, ledger) scroll horizontally on narrow screens instead of exploding the layout. - Hero / KPI / news-list layouts collapse cleanly at < 720px. 4 news tests; verified locally — home page renders, /sw.js serves with Service-Worker-Allowed: /, manifest is valid JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
73
tests/test_news.py
Normal file
73
tests/test_news.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Newsline — start-page digest aggregation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from psyc import db
|
||||
from psyc.lines import ledger as ledger_line
|
||||
from psyc.lines import news
|
||||
from psyc.models import Outcome, Severity, TLP
|
||||
from conftest import make_case
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_db(tmp_path, monkeypatch):
|
||||
test_db = tmp_path / "test.db"
|
||||
eng = create_engine(f"sqlite:///{test_db}", future=True)
|
||||
db._metadata.create_all(eng, checkfirst=True)
|
||||
monkeypatch.setattr(db, "_engine", eng)
|
||||
monkeypatch.setattr(db, "DB_PATH", test_db)
|
||||
yield test_db
|
||||
|
||||
|
||||
def test_kpis_count_basic(fresh_db):
|
||||
# vary age_days so make_case() doesn't collide its case_id
|
||||
db.upsert_case(make_case(feed="urlhaus", urls=["http://1.1.1.1/x"], severity=Severity.LOW, age_days=1))
|
||||
db.upsert_case(make_case(feed="feodo", ips=["2.2.2.2"], severity=Severity.HIGH, age_days=0))
|
||||
db.upsert_case(make_case(feed="urlhaus", urls=["http://3.3.3.3/x"], severity=Severity.CRITICAL, age_days=2))
|
||||
k = news.kpis()
|
||||
assert k["cases"] == 3
|
||||
assert k["high_total"] == 2 # high + critical
|
||||
|
||||
|
||||
def test_recent_items_interleaves_ledger_and_cases(fresh_db):
|
||||
c = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.HIGH)
|
||||
db.upsert_case(c)
|
||||
ledger_line.write(
|
||||
case_id=c.case_id, destination="CERT-Bund", payload_hash="",
|
||||
submitter_identity="x", tlp=TLP.AMBER, outcome=Outcome.ACTIONED,
|
||||
)
|
||||
items = news.recent_items(limit=10)
|
||||
kinds = {i.kind for i in items}
|
||||
assert "case" in kinds
|
||||
assert "enforced" in kinds
|
||||
# newest-first ordering
|
||||
assert items == sorted(items, key=lambda i: i.timestamp, reverse=True)
|
||||
|
||||
|
||||
def test_feed_health_groups_by_feed(fresh_db):
|
||||
db.upsert_case(make_case(feed="urlhaus", urls=["http://a/"], age_days=1))
|
||||
db.upsert_case(make_case(feed="urlhaus", urls=["http://b/"], age_days=2))
|
||||
db.upsert_case(make_case(feed="otx", ips=["1.1.1.1"], age_days=1))
|
||||
h = news.feed_health()
|
||||
by_feed = {f.feed: f for f in h}
|
||||
assert by_feed["urlhaus"].count == 2
|
||||
assert by_feed["otx"].count == 1
|
||||
# highest count first
|
||||
assert h[0].feed == "urlhaus"
|
||||
|
||||
|
||||
def test_outcome_kinds_match_render_map(fresh_db):
|
||||
# Every Outcome should produce a NewsItem (no KeyError).
|
||||
c = make_case(feed="urlhaus", ips=["1.2.3.4"])
|
||||
db.upsert_case(c)
|
||||
for outcome in Outcome:
|
||||
ledger_line.write(
|
||||
case_id=c.case_id, destination="X", payload_hash="",
|
||||
submitter_identity="x", tlp=TLP.AMBER, outcome=outcome,
|
||||
)
|
||||
items = news.recent_items(limit=200)
|
||||
# at least one item per outcome we wrote
|
||||
assert len([i for i in items if i.kind != "case"]) >= len(list(Outcome))
|
||||
Reference in New Issue
Block a user