重构:拆出 auth / blocking / runtimesettings / help / mqttforward 包

第二批:把根目录中纯逻辑领域文件(cache、service、admin route)按业务边界
迁到 internal/ 下的子包。各子包暴露 RegisterRoutes 给 web 包调用,根目录
只留下一行 bridge 文件保留旧名字别名。

新增包
- internal/auth/        SessionClaims / Manager / RequireAdmin / HashPassword /
                        VerifyPassword / AdminUserResponse 等。原 auth.go 中
                        被两个 admin route 依赖的 sessionClaims 现在以 auth.
                        SessionClaims 形式被它们 import;不再被锁在 main 包。
- internal/blocking/    Cache + RegisterRoutes,以前散在 blocking_cache.go
                        和 admin_blocking_routes.go 里。
- internal/runtimesettings/  Cache + RegisterRoutes。
- internal/help/        RenderMarkdown / RegisterPublicRoutes /
                        RegisterAdminRoutes(拆分原来的 registerHelpRoutes
                        和 registerAdminHelpRoutes 两条入口)。
- internal/mqttforward/ Manager / Reloader / Stats / RegisterRoutes。
                        forwarder runner、循环抑制 cache 等运行时逻辑随之迁入。
- internal/webutil/     ParseListOptions / WriteListResponse[WithTotal] /
                        ParseMapReportListOptions / ParseMapReportViewportOptions
                        以及 PtrString/PtrInt64/... 等指针解引用 helper。
                        以前散在 web.go 中,现在被各 admin route 子包共享,
                        避免 internal/blocking → internal/web → internal/blocking
                        的循环依赖。
- internal/store/testutil/  OpenStore(t) helper,让其它包测试零样板拿到 store。

根目录新增 bridge 文件
- blocking_bridge.go / runtime_settings_bridge.go / help_bridge.go /
  mqttforward_bridge.go:用 type alias + thin wrapper 把上述子包的导出
  名映射到旧的小写名(blockingCache、registerAdminBlockingRoutes 等),
  让 main.go / web.go 等仍未迁出的文件无须改动。

修改
- auth.go 改为对 internal/auth 的 bridge;web.go 中 sessions.newCookie /
  clearCookie 改为 NewCookie / ClearCookie。
- main_test.go 中 BlockingViolationForRecord* 测试不再直接构造未导出字段,
  改成走 store.CreateNodeBlocking → newBlockingCache 的真实路径。
- internal/mqttforward 把以前 *_store.go 中没有方法依赖的运行时类型
  (forwarder runner、loop cache)和 admin route 一并归位;mqtt_status.go
  暂时仍留在根目录(依赖 main 中的 mqttClientInfoFromClient)。

go build ./... / go test ./... 全部通过;测试数量未变。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 14:23:56 +08:00
co-authored by Claude
parent eff4972668
commit c527a9fd9a
23 changed files with 786 additions and 313 deletions
+102
View File
@@ -0,0 +1,102 @@
package blocking
import "testing"
func TestBlockingCacheLoadsEnabledRules(t *testing.T) {
st := openTestStore(t)
defer st.Close()
nodeNum := int64(305419896)
if _, err := st.CreateNodeBlocking("!12345678", &nodeNum, "enabled", true); err != nil {
t.Fatalf("CreateNodeBlocking(enabled) error = %v", err)
}
disabledNodeNum := int64(7)
if _, err := st.CreateNodeBlocking("!00000007", &disabledNodeNum, "disabled", false); err != nil {
t.Fatalf("CreateNodeBlocking(disabled) error = %v", err)
}
if _, err := st.CreateIPBlocking("192.168.1.0/24", "lan", true); err != nil {
t.Fatalf("CreateIPBlocking(cidr) error = %v", err)
}
if _, err := st.CreateIPBlocking("10.0.0.1", "disabled", false); err != nil {
t.Fatalf("CreateIPBlocking(disabled) error = %v", err)
}
if _, err := st.CreateForbiddenWordBlocking("spam", "contains", false, "enabled", true); err != nil {
t.Fatalf("CreateForbiddenWordBlocking(enabled) error = %v", err)
}
if _, err := st.CreateForbiddenWordBlocking("blocked", "contains", false, "disabled", false); err != nil {
t.Fatalf("CreateForbiddenWordBlocking(disabled) error = %v", err)
}
cache, err := New(st)
if err != nil {
t.Fatalf("New() error = %v", err)
}
if !cache.IsNodeBlocked("!12345678", nil) {
t.Fatal("IsNodeBlocked(enabled node id) = false, want true")
}
if !cache.IsNodeBlocked("", uint32(nodeNum)) {
t.Fatal("IsNodeBlocked(enabled node num) = false, want true")
}
if cache.IsNodeBlocked("!00000007", disabledNodeNum) {
t.Fatal("IsNodeBlocked(disabled node) = true, want false")
}
if !cache.IsIPBlocked("192.168.1.42") {
t.Fatal("IsIPBlocked(CIDR member) = false, want true")
}
if cache.IsIPBlocked("10.0.0.1") {
t.Fatal("IsIPBlocked(disabled IP) = true, want false")
}
if word, ok := cache.FindForbiddenWord("This is SPAM text"); !ok || word != "spam" {
t.Fatalf("FindForbiddenWord(case-insensitive) = %q, %v, want spam, true", word, ok)
}
if _, ok := cache.FindForbiddenWord("disabled blocked text"); ok {
t.Fatal("FindForbiddenWord(disabled word) = true, want false")
}
}
func TestBlockingCacheIPExactAndCIDR(t *testing.T) {
st := openTestStore(t)
defer st.Close()
if _, err := st.CreateIPBlocking("127.0.0.1", "loopback", true); err != nil {
t.Fatalf("CreateIPBlocking(ip) error = %v", err)
}
if _, err := st.CreateIPBlocking("2001:db8::/32", "docs", true); err != nil {
t.Fatalf("CreateIPBlocking(ipv6 cidr) error = %v", err)
}
cache, err := New(st)
if err != nil {
t.Fatalf("New() error = %v", err)
}
if !cache.IsIPBlocked("127.0.0.1") {
t.Fatal("IsIPBlocked(exact IPv4) = false, want true")
}
if !cache.IsIPBlocked("2001:db8::1") {
t.Fatal("IsIPBlocked(IPv6 CIDR) = false, want true")
}
if cache.IsIPBlocked("localhost") {
t.Fatal("IsIPBlocked(hostname) = true, want false")
}
}
func TestBlockingCacheForbiddenWordCaseSensitivity(t *testing.T) {
st := openTestStore(t)
defer st.Close()
if _, err := st.CreateForbiddenWordBlocking("Spam", "contains", true, "case-sensitive", true); err != nil {
t.Fatalf("CreateForbiddenWordBlocking(case-sensitive) error = %v", err)
}
cache, err := New(st)
if err != nil {
t.Fatalf("New() error = %v", err)
}
if _, ok := cache.FindForbiddenWord("lowercase spam"); ok {
t.Fatal("FindForbiddenWord(lowercase) = true, want false")
}
if word, ok := cache.FindForbiddenWord("contains Spam"); !ok || word != "Spam" {
t.Fatalf("FindForbiddenWord(exact case) = %q, %v, want Spam, true", word, ok)
}
}