Files
ros-dpi/backend/tests/test_engine.py
T
Su YijunandClaude Opus 5.5 f3e9fdb2af feat: ROS-DPI v0.1.0:RouterOS 终端流量与应用统计面板
- IPFIX / NetFlow v9 采集 + TZSP DNS 嗅探识别应用,可选 ROS 只读 REST 轮询
- 内置 100+ 应用规则库,支持自定义域名 / IP 段 / 端口规则
- 按分钟 / 小时 / 天汇总写入 MySQL
- 全网概览、主机监控、应用统计、流量流向(桑基图)、识别规则、系统设置
- 中英双语,深浅色主题,T568B 线色设计语言
- Docker 多架构镜像,GitHub Actions 测试与发布

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:51:02 +08:00

93 lines
4.2 KiB
Python

import time
from datetime import datetime
from types import SimpleNamespace
from zoneinfo import ZoneInfo
from packets import ipfix, tzsp_dns
from app import stats, timeutil
from app.classify import Classifier
from app.database import SessionLocal
from app.engine import Engine
Z = ZoneInfo("Asia/Shanghai")
def test_classifier_longest_suffix_and_cname():
c = Classifier()
assert c.classify("1.1.1.1", 6, 443, "v26-web.douyinvod.com", None) == "douyin"
assert c.classify("1.1.1.1", 6, 443, "y.qq.com", None) == "qq_music" # 比 qq.com 更具体
assert c.classify("1.1.1.1", 6, 443, "www.qq.com", None) == "qq"
assert c.classify("1.1.1.1", 6, 443, "unknown.example", "x.bilivideo.com") == "bilibili"
assert c.classify("91.108.4.5", 6, 443, None, None) == "telegram"
assert c.classify("9.9.9.9", 17, 443, None, None) == "quic"
assert c.classify("9.9.9.9", 6, 12345, None, None) == "other"
def test_custom_rule_overrides():
c = Classifier()
c.load_custom([SimpleNamespace(kind="domain", pattern="douyin.com", app_id="myapp", app_name="我的",
category="video")])
assert c.classify("1.1.1.1", 6, 443, "www.douyin.com", None) == "myapp"
assert c.apps["myapp"].name == "我的"
def _engine():
e = Engine()
e.set_lan("192.168.88.0/24,2408:8000:1:2::/64")
return e
def test_direction_dns_and_mac_learning():
e = _engine()
e.on_tzsp(tzsp_dns("192.168.88.10", "www.bilibili.com", ["5.5.5.5"]))
now = time.time()
up = e.add_flow(dict(src="192.168.88.10", dst="5.5.5.5", sport=1, dport=443, proto=6, bytes=100,
src_mac="AA:00:00:00:00:01"), now)
down = e.add_flow(dict(src="5.5.5.5", dst="192.168.88.10", sport=443, dport=1, proto=6, bytes=900), now)
assert up == ("AA:00:00:00:00:01", "bilibili", "up")
assert down == ("AA:00:00:00:00:01", "bilibili", "down")
# 内网互访、路由器自身流量不计
assert e.add_flow(dict(src="192.168.88.10", dst="192.168.88.20", proto=6, bytes=5), now) is None
assert e.add_flow(dict(src="100.64.1.2", dst="8.8.8.8", proto=17, bytes=5), now) is None
def test_post_nat_download():
e = _engine()
r = e.add_flow(dict(src="5.5.5.5", dst="100.64.1.2", post_dst="192.168.88.30", sport=443, dport=1, proto=6,
bytes=10), time.time())
assert r == ("IP:192.168.88.30", "https", "down")
def test_flush_and_ranges():
e = _engine()
e.on_tzsp(tzsp_dns("192.168.88.11", "store.steampowered.com", ["6.6.6.6"]))
e.on_ipfix(ipfix([dict(src="192.168.88.11", dst="6.6.6.6", sport=1, dport=443, proto=6, nbytes=1000,
mac="AA:00:00:00:00:11"),
dict(src="192.168.88.11", dst="7.7.7.7", sport=1, dport=443, proto=6, nbytes=50,
mac="AA:00:00:00:00:11")]), "10.0.0.1")
now = time.time()
e.flush(now)
e.add_flow(dict(src="6.6.6.6", dst="192.168.88.11", sport=443, dport=1, proto=6, bytes=4000), now)
e.flush(now) # 同一分钟再写一次,应累加
with SessionLocal() as db:
t = stats.total(db, *timeutil.range_bounds("today"), host="AA:00:00:00:00:11")
assert t == {"up": 1050, "down": 4000, "total": 5050, "flows": 3}
apps = {a["id"]: a for a in stats.top_apps(db, *timeutil.range_bounds("month"), host="AA:00:00:00:00:11")}
assert apps["steam"]["total"] == 5000 and apps["https"]["total"] == 50
hosts = {h["key"]: h for h in stats.all_hosts(db)}
assert hosts["AA:00:00:00:00:11"]["today"]["total"] == 5050
assert hosts["AA:00:00:00:00:11"]["online"]
doms = stats.host_domains(db, "AA:00:00:00:00:11", *timeutil.range_bounds("today"))
assert doms[0]["domain"] == "store.steampowered.com" and doms[0]["bytes"] == 5000
def test_range_bounds_week_starts_monday():
# 2026-10-21 是周三
now = datetime(2026, 10, 21, 15, 0, tzinfo=Z).timestamp()
start, _ = timeutil.range_bounds("week", now)
assert datetime.fromtimestamp(start, Z) == datetime(2026, 10, 19, tzinfo=Z)
start, _ = timeutil.range_bounds("month", now)
assert datetime.fromtimestamp(start, Z) == datetime(2026, 10, 1, tzinfo=Z)
assert timeutil.day_bucket(now) == datetime(2026, 10, 21, tzinfo=Z).timestamp()