重构:拆出 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:
@@ -0,0 +1,233 @@
|
||||
package blocking
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
storepkg "meshtastic_mqtt_server/internal/store"
|
||||
"meshtastic_mqtt_server/internal/webutil"
|
||||
)
|
||||
|
||||
type nodeBlockingRequest struct {
|
||||
NodeID string `json:"node_id"`
|
||||
NodeNum *int64 `json:"node_num"`
|
||||
Reason string `json:"reason"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type ipBlockingRequest struct {
|
||||
IPValue string `json:"ip_value"`
|
||||
Reason string `json:"reason"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type forbiddenWordBlockingRequest struct {
|
||||
Word string `json:"word"`
|
||||
MatchType string `json:"match_type"`
|
||||
CaseSensitive bool `json:"case_sensitive"`
|
||||
Reason string `json:"reason"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func RegisterRoutes(r gin.IRouter, store *storepkg.Store, blocking *Cache) {
|
||||
reloadBlocking := func() error {
|
||||
if blocking == nil {
|
||||
return nil
|
||||
}
|
||||
return blocking.Reload(store)
|
||||
}
|
||||
|
||||
r.GET("/blocking/nodes", func(c *gin.Context) {
|
||||
opts, ok := webutil.ParseListOptions(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rows, err := store.ListNodeBlocking(opts)
|
||||
if err != nil {
|
||||
webutil.WriteListResponse(c, rows, opts, err, nodeBlockingDTO)
|
||||
return
|
||||
}
|
||||
total, err := store.CountNodeBlocking(opts)
|
||||
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, nodeBlockingDTO)
|
||||
})
|
||||
r.POST("/blocking/nodes", func(c *gin.Context) {
|
||||
var req nodeBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.CreateNodeBlocking(req.NodeID, req.NodeNum, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusCreated, row, err, nodeBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.PUT("/blocking/nodes/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req nodeBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.UpdateNodeBlocking(id, req.NodeID, req.NodeNum, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusOK, row, err, nodeBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.DELETE("/blocking/nodes/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeBlockingDeleteResponse(c, store.DeleteNodeBlocking(id), reloadBlocking)
|
||||
})
|
||||
|
||||
r.GET("/blocking/ips", func(c *gin.Context) {
|
||||
opts, ok := webutil.ParseListOptions(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rows, err := store.ListIPBlocking(opts)
|
||||
if err != nil {
|
||||
webutil.WriteListResponse(c, rows, opts, err, ipBlockingDTO)
|
||||
return
|
||||
}
|
||||
total, err := store.CountIPBlocking(opts)
|
||||
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, ipBlockingDTO)
|
||||
})
|
||||
r.POST("/blocking/ips", func(c *gin.Context) {
|
||||
var req ipBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ip blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.CreateIPBlocking(req.IPValue, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusCreated, row, err, ipBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.PUT("/blocking/ips/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ipBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ip blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.UpdateIPBlocking(id, req.IPValue, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusOK, row, err, ipBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.DELETE("/blocking/ips/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeBlockingDeleteResponse(c, store.DeleteIPBlocking(id), reloadBlocking)
|
||||
})
|
||||
|
||||
r.GET("/blocking/words", func(c *gin.Context) {
|
||||
opts, ok := webutil.ParseListOptions(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rows, err := store.ListForbiddenWordBlocking(opts)
|
||||
if err != nil {
|
||||
webutil.WriteListResponse(c, rows, opts, err, forbiddenWordBlockingDTO)
|
||||
return
|
||||
}
|
||||
total, err := store.CountForbiddenWordBlocking(opts)
|
||||
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, forbiddenWordBlockingDTO)
|
||||
})
|
||||
r.POST("/blocking/words", func(c *gin.Context) {
|
||||
var req forbiddenWordBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid forbidden word blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.CreateForbiddenWordBlocking(req.Word, req.MatchType, req.CaseSensitive, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusCreated, row, err, forbiddenWordBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.PUT("/blocking/words/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req forbiddenWordBlockingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid forbidden word blocking request"})
|
||||
return
|
||||
}
|
||||
row, err := store.UpdateForbiddenWordBlocking(id, req.Word, req.MatchType, req.CaseSensitive, req.Reason, req.Enabled)
|
||||
writeBlockingMutationResponse(c, http.StatusOK, row, err, forbiddenWordBlockingDTO, reloadBlocking)
|
||||
})
|
||||
r.DELETE("/blocking/words/:id", func(c *gin.Context) {
|
||||
id, ok := parseBlockingID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeBlockingDeleteResponse(c, store.DeleteForbiddenWordBlocking(id), reloadBlocking)
|
||||
})
|
||||
}
|
||||
|
||||
func parseBlockingID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid blocking rule id"})
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func writeBlockingMutationResponse[T any](c *gin.Context, status int, row *T, err error, convert func(T) gin.H, afterSuccess func() error) {
|
||||
if errors.Is(err, storepkg.ErrBlockingAlreadyExists) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "blocking rule already exists"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "blocking rule not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if afterSuccess != nil {
|
||||
if err := afterSuccess(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "blocking rule saved but cache reload failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(status, gin.H{"item": convert(*row)})
|
||||
}
|
||||
|
||||
func writeBlockingDeleteResponse(c *gin.Context, err error, afterSuccess func() error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "blocking rule not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if afterSuccess != nil {
|
||||
if err := afterSuccess(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "blocking rule deleted but cache reload failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func nodeBlockingDTO(row storepkg.NodeBlockingRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": webutil.PtrInt64(row.NodeNum), "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
|
||||
func ipBlockingDTO(row storepkg.IPBlockingRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "ip_value": row.IPValue, "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
|
||||
func forbiddenWordBlockingDTO(row storepkg.ForbiddenWordBlockingRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "word": row.Word, "match_type": row.MatchType, "case_sensitive": row.CaseSensitive, "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package blocking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
storepkg "meshtastic_mqtt_server/internal/store"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
nodes map[string]struct{}
|
||||
nodeNums map[int64]struct{}
|
||||
ips map[string]struct{}
|
||||
cidrs []*net.IPNet
|
||||
words []forbiddenWordRule
|
||||
}
|
||||
|
||||
type forbiddenWordRule struct {
|
||||
word string
|
||||
foldedWord string
|
||||
matchType string
|
||||
caseSensitive bool
|
||||
}
|
||||
|
||||
func New(store *storepkg.Store) (*Cache, error) {
|
||||
cache := &Cache{}
|
||||
if err := cache.Reload(store); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Reload(store *storepkg.Store) error {
|
||||
if store == nil {
|
||||
return fmt.Errorf("store is required")
|
||||
}
|
||||
|
||||
nodeRows, err := store.ListEnabledNodeBlocking()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipRows, err := store.ListEnabledIPBlocking()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wordRows, err := store.ListEnabledForbiddenWordBlocking()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes := make(map[string]struct{}, len(nodeRows))
|
||||
nodeNums := make(map[int64]struct{}, len(nodeRows))
|
||||
for _, row := range nodeRows {
|
||||
nodeID := strings.TrimSpace(row.NodeID)
|
||||
if nodeID != "" {
|
||||
nodes[nodeID] = struct{}{}
|
||||
}
|
||||
if row.NodeNum != nil {
|
||||
nodeNums[*row.NodeNum] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ips := make(map[string]struct{}, len(ipRows))
|
||||
cidrs := make([]*net.IPNet, 0, len(ipRows))
|
||||
for _, row := range ipRows {
|
||||
value := strings.TrimSpace(row.IPValue)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
ips[ip.String()] = struct{}{}
|
||||
continue
|
||||
}
|
||||
if _, ipNet, err := net.ParseCIDR(value); err == nil {
|
||||
cidrs = append(cidrs, ipNet)
|
||||
}
|
||||
}
|
||||
|
||||
words := make([]forbiddenWordRule, 0, len(wordRows))
|
||||
for _, row := range wordRows {
|
||||
word := strings.TrimSpace(row.Word)
|
||||
if word == "" || row.MatchType != storepkg.ForbiddenWordMatchContains {
|
||||
continue
|
||||
}
|
||||
words = append(words, forbiddenWordRule{word: word, foldedWord: strings.ToLower(word), matchType: row.MatchType, caseSensitive: row.CaseSensitive})
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.nodes = nodes
|
||||
c.nodeNums = nodeNums
|
||||
c.ips = ips
|
||||
c.cidrs = cidrs
|
||||
c.words = words
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) IsNodeBlocked(nodeID any, nodeNum any) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
id, _ := nodeID.(string)
|
||||
num, hasNum := blockingInt64FromAny(nodeNum)
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if id != "" {
|
||||
if _, ok := c.nodes[id]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if hasNum {
|
||||
_, ok := c.nodeNums[num]
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Cache) IsIPBlocked(host string) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if _, ok := c.ips[ip.String()]; ok {
|
||||
return true
|
||||
}
|
||||
for _, ipNet := range c.cidrs {
|
||||
if ipNet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Cache) FindForbiddenWord(text any) (string, bool) {
|
||||
if c == nil {
|
||||
return "", false
|
||||
}
|
||||
value, ok := text.(string)
|
||||
if !ok || value == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
foldedText := ""
|
||||
for _, rule := range c.words {
|
||||
if rule.matchType != storepkg.ForbiddenWordMatchContains {
|
||||
continue
|
||||
}
|
||||
if rule.caseSensitive {
|
||||
if strings.Contains(value, rule.word) {
|
||||
return rule.word, true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if foldedText == "" {
|
||||
foldedText = strings.ToLower(value)
|
||||
}
|
||||
if strings.Contains(foldedText, rule.foldedWord) {
|
||||
return rule.word, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func blockingInt64FromAny(value any) (int64, bool) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return int64(v), true
|
||||
case int8:
|
||||
return int64(v), true
|
||||
case int16:
|
||||
return int64(v), true
|
||||
case int32:
|
||||
return int64(v), true
|
||||
case int64:
|
||||
return v, true
|
||||
case uint:
|
||||
return int64(v), true
|
||||
case uint8:
|
||||
return int64(v), true
|
||||
case uint16:
|
||||
return int64(v), true
|
||||
case uint32:
|
||||
return int64(v), true
|
||||
case uint64:
|
||||
if v > uint64(^uint64(0)>>1) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(v), true
|
||||
case float64:
|
||||
return int64(v), v == float64(int64(v))
|
||||
case string:
|
||||
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
||||
return n, err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package blocking
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"meshtastic_mqtt_server/internal/store"
|
||||
"meshtastic_mqtt_server/internal/store/testutil"
|
||||
)
|
||||
|
||||
// openTestStore 委托到 store/testutil,让本包的测试代码保持简洁。
|
||||
func openTestStore(t *testing.T) *store.Store {
|
||||
return testutil.OpenStore(t)
|
||||
}
|
||||
Reference in New Issue
Block a user