删除所有测试文件
This commit is contained in:
@@ -1,187 +0,0 @@
|
|||||||
package active
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"meshtastic_mqtt_server/internal/agenttool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mockActiveStore 是用于测试的 mock store
|
|
||||||
type mockActiveStore struct {
|
|
||||||
activeNodeCount int64
|
|
||||||
activeUserCount int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockActiveStore) CountActiveNodes(since time.Time) (int64, error) {
|
|
||||||
return m.activeNodeCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockActiveStore) CountActiveUsers(since time.Time) (int64, error) {
|
|
||||||
return m.activeUserCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestActiveTool_Query(t *testing.T) {
|
|
||||||
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
store := &mockActiveStore{
|
|
||||||
activeNodeCount: 25,
|
|
||||||
activeUserCount: 15,
|
|
||||||
}
|
|
||||||
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: true,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hours float64
|
|
||||||
queryType string
|
|
||||||
expectNodes bool
|
|
||||||
expectUsers bool
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "默认查询1小时(both)",
|
|
||||||
hours: 0, // 0 表示使用默认值
|
|
||||||
queryType: "",
|
|
||||||
expectNodes: true,
|
|
||||||
expectUsers: true,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询6小时",
|
|
||||||
hours: 6,
|
|
||||||
queryType: "both",
|
|
||||||
expectNodes: true,
|
|
||||||
expectUsers: true,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "仅查询节点",
|
|
||||||
hours: 1,
|
|
||||||
queryType: "nodes",
|
|
||||||
expectNodes: true,
|
|
||||||
expectUsers: false,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "仅查询人数",
|
|
||||||
hours: 1,
|
|
||||||
queryType: "users",
|
|
||||||
expectNodes: false,
|
|
||||||
expectUsers: true,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询24小时(最大值)",
|
|
||||||
hours: 24,
|
|
||||||
queryType: "both",
|
|
||||||
expectNodes: true,
|
|
||||||
expectUsers: true,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "超过24小时应限制到24小时",
|
|
||||||
hours: 48,
|
|
||||||
queryType: "both",
|
|
||||||
expectNodes: true,
|
|
||||||
expectUsers: true,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
params := activeParams{
|
|
||||||
Hours: tt.hours,
|
|
||||||
QueryType: tt.queryType,
|
|
||||||
}
|
|
||||||
argsJSON, _ := json.Marshal(params)
|
|
||||||
|
|
||||||
runtime := agenttool.Runtime{Now: now}
|
|
||||||
result, err := tool.Execute(context.Background(), string(argsJSON), runtime)
|
|
||||||
|
|
||||||
if tt.expectError && err == nil {
|
|
||||||
t.Errorf("Expected error but got none")
|
|
||||||
}
|
|
||||||
if !tt.expectError && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !tt.expectError {
|
|
||||||
t.Logf("Query result:\n%s", result)
|
|
||||||
|
|
||||||
// 验证结果包含预期的内容
|
|
||||||
if tt.expectNodes && result != "" {
|
|
||||||
// 应该包含节点统计
|
|
||||||
if !contains(result, "活跃节点") {
|
|
||||||
t.Errorf("Expected result to contain node count")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tt.expectUsers && result != "" {
|
|
||||||
// 应该包含人数统计
|
|
||||||
if !contains(result, "活跃人数") {
|
|
||||||
t.Errorf("Expected result to contain user count")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestActiveTool_Enabled(t *testing.T) {
|
|
||||||
// 测试工具启用状态
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
enabled bool
|
|
||||||
store ActiveStore
|
|
||||||
expect bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "启用且有store",
|
|
||||||
enabled: true,
|
|
||||||
store: &mockActiveStore{},
|
|
||||||
expect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "启用但无store",
|
|
||||||
enabled: true,
|
|
||||||
store: nil,
|
|
||||||
expect: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "禁用且有store",
|
|
||||||
enabled: false,
|
|
||||||
store: &mockActiveStore{},
|
|
||||||
expect: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: tt.enabled,
|
|
||||||
store: tt.store,
|
|
||||||
}
|
|
||||||
if tool.Enabled() != tt.expect {
|
|
||||||
t.Errorf("Expected Enabled() = %v, got %v", tt.expect, tool.Enabled())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsMiddle(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"meshtastic_mqtt_server/internal/agenttool"
|
|
||||||
storepkg "meshtastic_mqtt_server/internal/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mockSignStore 是用于测试的 mock store
|
|
||||||
type mockSignStore struct {
|
|
||||||
signs []storepkg.SignRecord
|
|
||||||
nodeInfoMap map[string]*storepkg.NodeInfoRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) CreateSign(nodeID string, longName, shortName *string, signText string, signTime time.Time) (*storepkg.SignRecord, error) {
|
|
||||||
record := storepkg.SignRecord{
|
|
||||||
NodeID: nodeID,
|
|
||||||
LongName: longName,
|
|
||||||
ShortName: shortName,
|
|
||||||
SignText: signText,
|
|
||||||
SignTime: signTime,
|
|
||||||
}
|
|
||||||
m.signs = append(m.signs, record)
|
|
||||||
return &record, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) HasSignedOnDay(nodeID string, day time.Time) (bool, error) {
|
|
||||||
loc := day.Location()
|
|
||||||
if loc == nil {
|
|
||||||
loc = time.Local
|
|
||||||
}
|
|
||||||
start := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, loc)
|
|
||||||
end := start.AddDate(0, 0, 1)
|
|
||||||
|
|
||||||
for _, sign := range m.signs {
|
|
||||||
if sign.NodeID == nodeID && sign.SignTime.After(start) && sign.SignTime.Before(end) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) GetNodeInfo(nodeID string) (*storepkg.NodeInfoRecord, error) {
|
|
||||||
return m.nodeInfoMap[nodeID], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) CountSigns(opts storepkg.ListOptions) (int64, error) {
|
|
||||||
count := int64(0)
|
|
||||||
for _, sign := range m.signs {
|
|
||||||
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) CountSignsByDay(opts storepkg.ListOptions) ([]storepkg.SignDayCount, error) {
|
|
||||||
dayCounts := make(map[string]int64)
|
|
||||||
for _, sign := range m.signs {
|
|
||||||
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dateStr := sign.SignTime.Format("2006-01-02")
|
|
||||||
dayCounts[dateStr]++
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []storepkg.SignDayCount
|
|
||||||
for date, count := range dayCounts {
|
|
||||||
result = append(result, storepkg.SignDayCount{Date: date, Count: count})
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockSignStore) ListSigns(opts storepkg.ListOptions) ([]storepkg.SignRecord, error) {
|
|
||||||
var result []storepkg.SignRecord
|
|
||||||
for _, sign := range m.signs {
|
|
||||||
// 过滤 NodeID
|
|
||||||
if opts.NodeID != "" && sign.NodeID != opts.NodeID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 过滤时间范围
|
|
||||||
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, sign)
|
|
||||||
}
|
|
||||||
// 应用 Limit
|
|
||||||
if opts.Limit > 0 && len(result) > opts.Limit {
|
|
||||||
result = result[:opts.Limit]
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignTool_Query(t *testing.T) {
|
|
||||||
// 创建 mock store 并添加测试数据
|
|
||||||
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
|
|
||||||
yesterday := now.AddDate(0, 0, -1)
|
|
||||||
twoDaysAgo := now.AddDate(0, 0, -2)
|
|
||||||
|
|
||||||
store := &mockSignStore{
|
|
||||||
signs: []storepkg.SignRecord{
|
|
||||||
{NodeID: "node1", SignText: "上海-Alice-Device1签到", SignTime: now},
|
|
||||||
{NodeID: "node2", SignText: "北京-Bob-Device2签到", SignTime: now},
|
|
||||||
{NodeID: "node3", SignText: "深圳-Charlie-Device3签到", SignTime: yesterday},
|
|
||||||
{NodeID: "node4", SignText: "广州-David-Device4签到", SignTime: twoDaysAgo},
|
|
||||||
},
|
|
||||||
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
|
||||||
}
|
|
||||||
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: true,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
action string
|
|
||||||
date string
|
|
||||||
days int
|
|
||||||
expectCount int64
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "查询今天",
|
|
||||||
action: "query",
|
|
||||||
date: "2024-06-23",
|
|
||||||
days: 0,
|
|
||||||
expectCount: 2,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询最近3天",
|
|
||||||
action: "query",
|
|
||||||
date: "2024-06-23",
|
|
||||||
days: 3,
|
|
||||||
expectCount: 4,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询昨天",
|
|
||||||
action: "query",
|
|
||||||
date: "2024-06-22",
|
|
||||||
days: 0,
|
|
||||||
expectCount: 1,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
params := signParams{
|
|
||||||
Action: tt.action,
|
|
||||||
Date: tt.date,
|
|
||||||
Days: tt.days,
|
|
||||||
}
|
|
||||||
argsJSON, _ := json.Marshal(params)
|
|
||||||
|
|
||||||
runtime := agenttool.Runtime{Now: now}
|
|
||||||
result, err := tool.Execute(context.Background(), string(argsJSON), runtime)
|
|
||||||
|
|
||||||
if tt.expectError && err == nil {
|
|
||||||
t.Errorf("Expected error but got none")
|
|
||||||
}
|
|
||||||
if !tt.expectError && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !tt.expectError {
|
|
||||||
t.Logf("Query result:\n%s", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignTool_SignAction(t *testing.T) {
|
|
||||||
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
|
|
||||||
store := &mockSignStore{
|
|
||||||
signs: []storepkg.SignRecord{},
|
|
||||||
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
|
||||||
}
|
|
||||||
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: true,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试签到功能
|
|
||||||
params := signParams{
|
|
||||||
Action: "sign",
|
|
||||||
Region: "上海闵行",
|
|
||||||
Name: "TestUser",
|
|
||||||
Device: "TestDevice",
|
|
||||||
}
|
|
||||||
argsJSON, _ := json.Marshal(params)
|
|
||||||
|
|
||||||
// 创建带节点上下文的 context
|
|
||||||
nodeCtx := agenttool.NodeContext{
|
|
||||||
NodeID: "test_node_123",
|
|
||||||
LongName: "Test Node",
|
|
||||||
ShortName: "TN",
|
|
||||||
}
|
|
||||||
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
|
|
||||||
|
|
||||||
runtime := agenttool.Runtime{Now: now}
|
|
||||||
result, err := tool.Execute(ctx, string(argsJSON), runtime)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Sign result: %s", result)
|
|
||||||
|
|
||||||
// 验证签到记录已创建
|
|
||||||
if len(store.signs) != 1 {
|
|
||||||
t.Errorf("Expected 1 sign record, got %d", len(store.signs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignTool_CheckAction(t *testing.T) {
|
|
||||||
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// 测试场景1:今天未签到
|
|
||||||
t.Run("今天未签到", func(t *testing.T) {
|
|
||||||
store := &mockSignStore{
|
|
||||||
signs: []storepkg.SignRecord{},
|
|
||||||
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
|
||||||
}
|
|
||||||
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: true,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
|
|
||||||
params := signParams{
|
|
||||||
Action: "check",
|
|
||||||
}
|
|
||||||
argsJSON, _ := json.Marshal(params)
|
|
||||||
|
|
||||||
nodeCtx := agenttool.NodeContext{
|
|
||||||
NodeID: "test_node_123",
|
|
||||||
LongName: "Test Node",
|
|
||||||
ShortName: "TN",
|
|
||||||
}
|
|
||||||
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
|
|
||||||
|
|
||||||
runtime := agenttool.Runtime{Now: now}
|
|
||||||
result, err := tool.Execute(ctx, string(argsJSON), runtime)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Check result (not signed): %s", result)
|
|
||||||
|
|
||||||
if !contains(result, "还没有签到") && !contains(result, "没有签到") {
|
|
||||||
t.Errorf("Expected result to indicate not signed yet")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 测试场景2:今天已签到
|
|
||||||
t.Run("今天已签到", func(t *testing.T) {
|
|
||||||
signTime := time.Date(2024, 6, 23, 10, 30, 45, 0, time.UTC)
|
|
||||||
store := &mockSignStore{
|
|
||||||
signs: []storepkg.SignRecord{
|
|
||||||
{
|
|
||||||
NodeID: "test_node_123",
|
|
||||||
SignText: "上海-TestUser-TestDevice签到",
|
|
||||||
SignTime: signTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
|
||||||
}
|
|
||||||
|
|
||||||
tool := &Tool{
|
|
||||||
enabled: true,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
|
|
||||||
params := signParams{
|
|
||||||
Action: "check",
|
|
||||||
}
|
|
||||||
argsJSON, _ := json.Marshal(params)
|
|
||||||
|
|
||||||
nodeCtx := agenttool.NodeContext{
|
|
||||||
NodeID: "test_node_123",
|
|
||||||
LongName: "Test Node",
|
|
||||||
ShortName: "TN",
|
|
||||||
}
|
|
||||||
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
|
|
||||||
|
|
||||||
runtime := agenttool.Runtime{Now: now}
|
|
||||||
result, err := tool.Execute(ctx, string(argsJSON), runtime)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Check result (already signed): %s", result)
|
|
||||||
|
|
||||||
if !contains(result, "已经签到") {
|
|
||||||
t.Errorf("Expected result to indicate already signed")
|
|
||||||
}
|
|
||||||
if !contains(result, "签到时间") {
|
|
||||||
t.Errorf("Expected result to contain sign time")
|
|
||||||
}
|
|
||||||
if !contains(result, "10:30:45") {
|
|
||||||
t.Errorf("Expected result to contain the exact sign time")
|
|
||||||
}
|
|
||||||
if !contains(result, "签到内容") {
|
|
||||||
t.Errorf("Expected result to contain sign text")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsMiddle(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadConfigCreatesDefaultFile(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
|
|
||||||
|
|
||||||
cfg, err := Load(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load() error = %v", err)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.Host != "0.0.0.0" {
|
|
||||||
t.Fatalf("host = %q, want 0.0.0.0", cfg.MQTT.Host)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.Port != 1883 {
|
|
||||||
t.Fatalf("port = %d, want 1883", cfg.MQTT.Port)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.TLS.Enabled {
|
|
||||||
t.Fatalf("tls enabled = true, want false")
|
|
||||||
}
|
|
||||||
if cfg.Meshtastic.PSK != "AQ==" {
|
|
||||||
t.Fatalf("psk = %q, want AQ==", cfg.Meshtastic.PSK)
|
|
||||||
}
|
|
||||||
if cfg.Database.Driver != "sqlite" {
|
|
||||||
t.Fatalf("database driver = %q, want sqlite", cfg.Database.Driver)
|
|
||||||
}
|
|
||||||
if cfg.Database.SQLite.Path == "" {
|
|
||||||
t.Fatalf("sqlite path is empty")
|
|
||||||
}
|
|
||||||
if !cfg.Web.Enabled {
|
|
||||||
t.Fatalf("web enabled = false, want true")
|
|
||||||
}
|
|
||||||
if !cfg.Web.PortEnabled {
|
|
||||||
t.Fatalf("web port enabled = false, want true")
|
|
||||||
}
|
|
||||||
wantSocketEnabled := defaultWebSocketPath() != ""
|
|
||||||
if cfg.Web.SocketEnabled != wantSocketEnabled {
|
|
||||||
t.Fatalf("web socket enabled = %t, want %t", cfg.Web.SocketEnabled, wantSocketEnabled)
|
|
||||||
}
|
|
||||||
if cfg.Web.Port != 8080 {
|
|
||||||
t.Fatalf("web port = %d, want 8080", cfg.Web.Port)
|
|
||||||
}
|
|
||||||
if cfg.Web.SocketPath != defaultWebSocketPath() {
|
|
||||||
t.Fatalf("web socket path = %q, want %q", cfg.Web.SocketPath, defaultWebSocketPath())
|
|
||||||
}
|
|
||||||
if cfg.Web.StaticDir != "./dist" {
|
|
||||||
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
|
|
||||||
}
|
|
||||||
if cfg.Web.MapTileCacheDir != defaultMapTileCacheDir() {
|
|
||||||
t.Fatalf("web map tile cache dir = %q, want %q", cfg.Web.MapTileCacheDir, defaultMapTileCacheDir())
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
t.Fatalf("default config was not written: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadConfigFillsMissingFields(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte("mqtt:\n port: 1884\n"), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load() error = %v", err)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.Port != 1884 {
|
|
||||||
t.Fatalf("port = %d, want 1884", cfg.MQTT.Port)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.Host != "0.0.0.0" {
|
|
||||||
t.Fatalf("host = %q, want 0.0.0.0", cfg.MQTT.Host)
|
|
||||||
}
|
|
||||||
if cfg.Meshtastic.PSK != "AQ==" {
|
|
||||||
t.Fatalf("psk = %q, want AQ==", cfg.Meshtastic.PSK)
|
|
||||||
}
|
|
||||||
if cfg.Database.Driver != "sqlite" {
|
|
||||||
t.Fatalf("database driver = %q, want sqlite", cfg.Database.Driver)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
text := string(data)
|
|
||||||
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port_enabled:", "socket_enabled:", "port:", "socket_path:", "static_dir:", "map_tile_cache_dir:"} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("completed config missing %q in:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadConfigPreservesExplicitFalse(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
content := "mqtt:\n host: 127.0.0.1\n port: 1885\n tls:\n enabled: false\n cert_file: cert.pem\n key_file: key.pem\nmeshtastic:\n psk: AQ==\ndatabase:\n driver: sqlite\n sqlite:\n path: test.db\n mysql:\n dsn: \"\"\n"
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load() error = %v", err)
|
|
||||||
}
|
|
||||||
if cfg.MQTT.TLS.Enabled {
|
|
||||||
t.Fatalf("tls enabled = true, want explicit false")
|
|
||||||
}
|
|
||||||
if cfg.MQTT.TLS.CertFile != "cert.pem" || cfg.MQTT.TLS.KeyFile != "key.pem" {
|
|
||||||
t.Fatalf("tls paths = %q/%q, want cert.pem/key.pem", cfg.MQTT.TLS.CertFile, cfg.MQTT.TLS.KeyFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadConfigPreservesExplicitWebFalse(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
content := "web:\n enabled: false\n port_enabled: false\n socket_enabled: false\n host: 127.0.0.1\n port: 8081\n static_dir: ./public\n"
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load() error = %v", err)
|
|
||||||
}
|
|
||||||
if cfg.Web.Enabled {
|
|
||||||
t.Fatalf("web enabled = true, want explicit false")
|
|
||||||
}
|
|
||||||
if cfg.Web.PortEnabled || cfg.Web.SocketEnabled {
|
|
||||||
t.Fatalf("web listener enabled = %t/%t, want explicit false/false", cfg.Web.PortEnabled, cfg.Web.SocketEnabled)
|
|
||||||
}
|
|
||||||
if cfg.Web.Host != "127.0.0.1" || cfg.Web.Port != 8081 || cfg.Web.StaticDir != "./public" {
|
|
||||||
t.Fatalf("web config = %#v", cfg.Web)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadConfigMalformedYAMLDoesNotOverwrite(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
content := "mqtt:\n port: [\n"
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := Load(path)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Load() error = nil, want parse error")
|
|
||||||
}
|
|
||||||
data, readErr := os.ReadFile(path)
|
|
||||||
if readErr != nil {
|
|
||||||
t.Fatal(readErr)
|
|
||||||
}
|
|
||||||
if string(data) != content {
|
|
||||||
t.Fatalf("malformed config was overwritten: %q", string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultConfigDirForGOOS(t *testing.T) {
|
|
||||||
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go")
|
|
||||||
for _, goos := range []string{"windows", "darwin"} {
|
|
||||||
path := defaultConfigDirForGOOS(goos)
|
|
||||||
if path != wantRelative {
|
|
||||||
t.Fatalf("%s config dir = %q, want %q", goos, path, wantRelative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linuxPath := defaultConfigDirForGOOS("linux")
|
|
||||||
wantLinux := filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go")
|
|
||||||
if linuxPath != wantLinux {
|
|
||||||
t.Fatalf("linux config dir = %q, want %q", linuxPath, wantLinux)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultMapTileCacheDirForGOOS(t *testing.T) {
|
|
||||||
wantRelative := filepath.Join(".", "win", "srv", "mesh_mqtt_go")
|
|
||||||
for _, goos := range []string{"windows", "darwin"} {
|
|
||||||
path := defaultMapTileCacheDirForGOOS(goos)
|
|
||||||
if path != wantRelative {
|
|
||||||
t.Fatalf("%s map tile cache dir = %q, want %q", goos, path, wantRelative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linuxPath := defaultMapTileCacheDirForGOOS("linux")
|
|
||||||
wantLinux := filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
|
|
||||||
if linuxPath != wantLinux {
|
|
||||||
t.Fatalf("linux map tile cache dir = %q, want %q", linuxPath, wantLinux)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultWebSocketPathForGOOS(t *testing.T) {
|
|
||||||
if windowsPath := defaultWebSocketPathForGOOS("windows"); windowsPath != "" {
|
|
||||||
t.Fatalf("windows web socket path = %q, want empty", windowsPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
darwinPath := defaultWebSocketPathForGOOS("darwin")
|
|
||||||
wantDarwin := filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
|
|
||||||
if darwinPath != wantDarwin {
|
|
||||||
t.Fatalf("darwin web socket path = %q, want %q", darwinPath, wantDarwin)
|
|
||||||
}
|
|
||||||
|
|
||||||
linuxPath := defaultWebSocketPathForGOOS("linux")
|
|
||||||
want := filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
|
|
||||||
if linuxPath != want {
|
|
||||||
t.Fatalf("linux web socket path = %q, want %q", linuxPath, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearWebSocketPathOnUnsupportedGOOS(t *testing.T) {
|
|
||||||
cfg := Default()
|
|
||||||
cfg.Web.SocketPath = filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
|
|
||||||
if !ClearWebSocketPathOnUnsupportedGOOS(cfg, "windows") {
|
|
||||||
t.Fatalf("ClearWebSocketPathOnUnsupportedGOOS() = false, want true")
|
|
||||||
}
|
|
||||||
if cfg.Web.SocketPath != "" {
|
|
||||||
t.Fatalf("windows web socket path = %q, want empty", cfg.Web.SocketPath)
|
|
||||||
}
|
|
||||||
if cfg.Web.SocketEnabled {
|
|
||||||
t.Fatalf("windows web socket enabled = true, want false")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Web.SocketPath = "/opt/mesh_mqtt_go/web.sock"
|
|
||||||
if ClearWebSocketPathOnUnsupportedGOOS(cfg, "linux") {
|
|
||||||
t.Fatalf("linux ClearWebSocketPathOnUnsupportedGOOS() = true, want false")
|
|
||||||
}
|
|
||||||
if cfg.Web.SocketPath == "" {
|
|
||||||
t.Fatalf("linux web socket path was cleared")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultSQLitePathForGOOS(t *testing.T) {
|
|
||||||
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
|
|
||||||
for _, goos := range []string{"windows", "darwin"} {
|
|
||||||
path := defaultSQLitePathForGOOS(goos)
|
|
||||||
if path != wantRelative {
|
|
||||||
t.Fatalf("%s sqlite path = %q, want %q", goos, path, wantRelative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linuxPath := defaultSQLitePathForGOOS("linux")
|
|
||||||
want := filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go", "mesh_mqtt_go.db")
|
|
||||||
if linuxPath != want {
|
|
||||||
t.Fatalf("linux sqlite path = %q, want %q", linuxPath, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateConfigDatabase(t *testing.T) {
|
|
||||||
cfg := Default()
|
|
||||||
cfg.Database.Driver = "postgres"
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.driver") {
|
|
||||||
t.Fatalf("invalid driver error = %v, want database.driver error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Database.SQLite.Path = ""
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.sqlite.path") {
|
|
||||||
t.Fatalf("missing sqlite path error = %v, want database.sqlite.path error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Database.Driver = "mysql"
|
|
||||||
cfg.Database.MySQL.DSN = ""
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.mysql.dsn") {
|
|
||||||
t.Fatalf("missing mysql dsn error = %v, want database.mysql.dsn error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateConfigWeb(t *testing.T) {
|
|
||||||
cfg := Default()
|
|
||||||
cfg.Web.Port = 0
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
|
|
||||||
t.Fatalf("invalid web port error = %v, want web port error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.PortEnabled = false
|
|
||||||
cfg.Web.Port = 0
|
|
||||||
if err := Validate(cfg); err != nil {
|
|
||||||
t.Fatalf("disabled web port with invalid port error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.SocketEnabled = false
|
|
||||||
cfg.Web.SocketPath = ""
|
|
||||||
if err := Validate(cfg); err != nil {
|
|
||||||
t.Fatalf("disabled web socket with empty path error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.PortEnabled = false
|
|
||||||
cfg.Web.SocketEnabled = true
|
|
||||||
cfg.Web.SocketPath = ""
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.socket_path") {
|
|
||||||
t.Fatalf("missing web socket path error = %v, want web.socket_path error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.PortEnabled = false
|
|
||||||
cfg.Web.SocketEnabled = false
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.port_enabled") {
|
|
||||||
t.Fatalf("disabled web listeners error = %v, want web.port_enabled error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.StaticDir = ""
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") {
|
|
||||||
t.Fatalf("missing web static dir error = %v, want web.static_dir error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.MapTileCacheDir = ""
|
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.map_tile_cache_dir") {
|
|
||||||
t.Fatalf("missing map tile cache dir error = %v, want web.map_tile_cache_dir error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = Default()
|
|
||||||
cfg.Web.Enabled = false
|
|
||||||
cfg.Web.PortEnabled = false
|
|
||||||
cfg.Web.SocketEnabled = false
|
|
||||||
cfg.Web.Port = 0
|
|
||||||
cfg.Web.StaticDir = ""
|
|
||||||
if err := Validate(cfg); err != nil {
|
|
||||||
t.Fatalf("disabled web validate error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildTLSConfigDisabled(t *testing.T) {
|
|
||||||
cfg, err := BuildTLS(TLSConfig{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildTLS() error = %v", err)
|
|
||||||
}
|
|
||||||
if cfg != nil {
|
|
||||||
t.Fatalf("BuildTLS() = %#v, want nil", cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildTLSConfigRequiresCertAndKey(t *testing.T) {
|
|
||||||
_, err := BuildTLS(TLSConfig{Enabled: true})
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "cert_file") {
|
|
||||||
t.Fatalf("missing cert error = %v, want cert_file error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = BuildTLS(TLSConfig{Enabled: true, CertFile: "cert.pem"})
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "key_file") {
|
|
||||||
t.Fatalf("missing key error = %v, want key_file error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package llm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUpdateProvider(t *testing.T) {
|
|
||||||
// Create initial state with one provider
|
|
||||||
configs := []ProviderConfig{
|
|
||||||
{
|
|
||||||
Name: "test-provider",
|
|
||||||
Active: true,
|
|
||||||
APIKey: "test-key",
|
|
||||||
BaseURL: "https://test.example.com",
|
|
||||||
Model: "test-model",
|
|
||||||
Timeout: 120,
|
|
||||||
ContextWindowTokens: 4096,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := NewState(configs)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the initial profile
|
|
||||||
profile := state.ActiveProfile()
|
|
||||||
if profile.Config.APIKey != "test-key" {
|
|
||||||
t.Errorf("expected APIKey 'test-key', got '%s'", profile.Config.APIKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the provider with new config
|
|
||||||
updatedConfig := ProviderConfig{
|
|
||||||
Name: "test-provider",
|
|
||||||
Active: true,
|
|
||||||
APIKey: "new-key",
|
|
||||||
BaseURL: "https://new.example.com",
|
|
||||||
Model: "new-model",
|
|
||||||
Timeout: 60,
|
|
||||||
ContextWindowTokens: 8192,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = state.UpdateProvider(updatedConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to update provider: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the update
|
|
||||||
profile = state.ActiveProfile()
|
|
||||||
if profile.Config.APIKey != "new-key" {
|
|
||||||
t.Errorf("expected updated APIKey 'new-key', got '%s'", profile.Config.APIKey)
|
|
||||||
}
|
|
||||||
if profile.Config.BaseURL != "https://new.example.com" {
|
|
||||||
t.Errorf("expected updated BaseURL 'https://new.example.com', got '%s'", profile.Config.BaseURL)
|
|
||||||
}
|
|
||||||
if profile.Config.Model != "new-model" {
|
|
||||||
t.Errorf("expected updated Model 'new-model', got '%s'", profile.Config.Model)
|
|
||||||
}
|
|
||||||
if profile.Config.Timeout != 60 {
|
|
||||||
t.Errorf("expected updated Timeout 60, got %d", profile.Config.Timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddProvider(t *testing.T) {
|
|
||||||
// Create initial state with one provider
|
|
||||||
configs := []ProviderConfig{
|
|
||||||
{
|
|
||||||
Name: "provider1",
|
|
||||||
Active: true,
|
|
||||||
APIKey: "key1",
|
|
||||||
BaseURL: "https://example1.com",
|
|
||||||
Model: "model1",
|
|
||||||
Timeout: 120,
|
|
||||||
ContextWindowTokens: 4096,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := NewState(configs)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a second provider
|
|
||||||
newConfig := ProviderConfig{
|
|
||||||
Name: "provider2",
|
|
||||||
Active: false,
|
|
||||||
APIKey: "key2",
|
|
||||||
BaseURL: "https://example2.com",
|
|
||||||
Model: "model2",
|
|
||||||
Timeout: 60,
|
|
||||||
ContextWindowTokens: 8192,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = state.AddProvider(newConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to add provider: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the new provider exists
|
|
||||||
profile, err := state.GetProfile("provider2")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get provider2: %v", err)
|
|
||||||
}
|
|
||||||
if profile.Config.Name != "provider2" {
|
|
||||||
t.Errorf("expected name 'provider2', got '%s'", profile.Config.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify active provider is still provider1
|
|
||||||
activeProfile := state.ActiveProfile()
|
|
||||||
if activeProfile.Config.Name != "provider1" {
|
|
||||||
t.Errorf("expected active provider 'provider1', got '%s'", activeProfile.Config.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveProvider(t *testing.T) {
|
|
||||||
// Create initial state with two providers
|
|
||||||
configs := []ProviderConfig{
|
|
||||||
{
|
|
||||||
Name: "provider1",
|
|
||||||
Active: true,
|
|
||||||
APIKey: "key1",
|
|
||||||
BaseURL: "https://example1.com",
|
|
||||||
Model: "model1",
|
|
||||||
Timeout: 120,
|
|
||||||
ContextWindowTokens: 4096,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "provider2",
|
|
||||||
Active: false,
|
|
||||||
APIKey: "key2",
|
|
||||||
BaseURL: "https://example2.com",
|
|
||||||
Model: "model2",
|
|
||||||
Timeout: 60,
|
|
||||||
ContextWindowTokens: 8192,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := NewState(configs)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove provider2
|
|
||||||
err = state.RemoveProvider("provider2")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to remove provider: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify provider2 is gone
|
|
||||||
_, err = state.GetProfile("provider2")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when getting removed provider, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to remove the last provider (should fail)
|
|
||||||
err = state.RemoveProvider("provider1")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when removing last provider, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
package mqtpp
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestBuildTextMessageServiceEnvelopeRoundTrip(t *testing.T) {
|
|
||||||
key, err := ExpandPSK("AQ==")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExpandPSK() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := BuildTextMessageServiceEnvelope(TextMessageBuildOptions{
|
|
||||||
PacketBuildOptions: PacketBuildOptions{
|
|
||||||
FromNodeNum: 0x12345678,
|
|
||||||
ToNodeNum: NodeNumBroadcast,
|
|
||||||
PacketID: 0x87654321,
|
|
||||||
ChannelID: "LongFast",
|
|
||||||
GatewayID: "!12345678",
|
|
||||||
PSK: key,
|
|
||||||
Encrypt: true,
|
|
||||||
ViaMQTT: true,
|
|
||||||
},
|
|
||||||
Text: "hello from bot",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildTextMessageServiceEnvelope() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
|
||||||
}
|
|
||||||
if record["type"] != "text_message" {
|
|
||||||
t.Fatalf("record type = %v", record["type"])
|
|
||||||
}
|
|
||||||
if record["text"] != "hello from bot" {
|
|
||||||
t.Fatalf("text = %v", record["text"])
|
|
||||||
}
|
|
||||||
if record["from_num"] != uint32(0x12345678) {
|
|
||||||
t.Fatalf("from_num = %v", record["from_num"])
|
|
||||||
}
|
|
||||||
if record["packet_to_num"] != uint32(NodeNumBroadcast) {
|
|
||||||
t.Fatalf("packet_to_num = %v", record["packet_to_num"])
|
|
||||||
}
|
|
||||||
if record["decrypt_success"] != true {
|
|
||||||
t.Fatalf("decrypt_success = %v", record["decrypt_success"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildTextMessageServiceEnvelopeDirectRoundTrip(t *testing.T) {
|
|
||||||
key, err := ExpandPSK("AQ==")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExpandPSK() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := BuildTextMessageServiceEnvelope(TextMessageBuildOptions{
|
|
||||||
PacketBuildOptions: PacketBuildOptions{
|
|
||||||
FromNodeNum: 0x12345678,
|
|
||||||
ToNodeNum: 0x10203040,
|
|
||||||
PacketID: 0x11111111,
|
|
||||||
ChannelID: "LongFast",
|
|
||||||
GatewayID: "!12345678",
|
|
||||||
PSK: key,
|
|
||||||
Encrypt: true,
|
|
||||||
ViaMQTT: true,
|
|
||||||
},
|
|
||||||
Text: "direct hello",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildTextMessageServiceEnvelope() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
|
||||||
}
|
|
||||||
if record["text"] != "direct hello" {
|
|
||||||
t.Fatalf("text = %v", record["text"])
|
|
||||||
}
|
|
||||||
if record["packet_to"] != "!10203040" {
|
|
||||||
t.Fatalf("packet_to = %v", record["packet_to"])
|
|
||||||
}
|
|
||||||
if record["packet_to_num"] != uint32(0x10203040) {
|
|
||||||
t.Fatalf("packet_to_num = %v", record["packet_to_num"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildNodeInfoServiceEnvelopeRoundTrip(t *testing.T) {
|
|
||||||
key, err := ExpandPSK("AQ==")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExpandPSK() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := BuildNodeInfoServiceEnvelope(NodeInfoBuildOptions{
|
|
||||||
PacketBuildOptions: PacketBuildOptions{
|
|
||||||
FromNodeNum: 0x12345678,
|
|
||||||
ToNodeNum: NodeNumBroadcast,
|
|
||||||
PacketID: 0x22222222,
|
|
||||||
ChannelID: "LongFast",
|
|
||||||
GatewayID: "!12345678",
|
|
||||||
PSK: key,
|
|
||||||
Encrypt: true,
|
|
||||||
ViaMQTT: true,
|
|
||||||
},
|
|
||||||
NodeID: "!12345678",
|
|
||||||
LongName: "MQTT Bot",
|
|
||||||
ShortName: "BT",
|
|
||||||
HWModel: 255,
|
|
||||||
Role: 0,
|
|
||||||
IsLicensed: false,
|
|
||||||
PublicKey: []byte{1, 2, 3},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildNodeInfoServiceEnvelope() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
|
||||||
}
|
|
||||||
if record["type"] != "nodeinfo" {
|
|
||||||
t.Fatalf("record type = %v", record["type"])
|
|
||||||
}
|
|
||||||
if record["long_name"] != "MQTT Bot" {
|
|
||||||
t.Fatalf("long_name = %v", record["long_name"])
|
|
||||||
}
|
|
||||||
if record["short_name"] != "BT" {
|
|
||||||
t.Fatalf("short_name = %v", record["short_name"])
|
|
||||||
}
|
|
||||||
if record["hw_model"] != "PRIVATE_HW" {
|
|
||||||
t.Fatalf("hw_model = %v", record["hw_model"])
|
|
||||||
}
|
|
||||||
if record["role"] != "CLIENT" {
|
|
||||||
t.Fatalf("role = %v", record["role"])
|
|
||||||
}
|
|
||||||
if record["is_licensed"] != false {
|
|
||||||
t.Fatalf("is_licensed = %v", record["is_licensed"])
|
|
||||||
}
|
|
||||||
if record["public_key"] != "010203" {
|
|
||||||
t.Fatalf("public_key = %v", record["public_key"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildNodeInfoTruncatesNanopbStrings(t *testing.T) {
|
|
||||||
key, err := ExpandPSK("AQ==")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExpandPSK() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := BuildNodeInfoServiceEnvelope(NodeInfoBuildOptions{
|
|
||||||
PacketBuildOptions: PacketBuildOptions{FromNodeNum: 0x12345678, ToNodeNum: NodeNumBroadcast, PacketID: 0x33333333, ChannelID: "LongFast", GatewayID: "!12345678", PSK: key, Encrypt: true, ViaMQTT: true},
|
|
||||||
NodeID: "!12345678",
|
|
||||||
LongName: "这是一个非常非常非常非常长的机器人节点名称",
|
|
||||||
ShortName: "机器人",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildNodeInfoServiceEnvelope() error = %v", err)
|
|
||||||
}
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
|
||||||
}
|
|
||||||
if len([]byte(record["long_name"].(string))) > 40 {
|
|
||||||
t.Fatalf("long_name byte length = %d", len([]byte(record["long_name"].(string))))
|
|
||||||
}
|
|
||||||
if len([]byte(record["short_name"].(string))) > 5 {
|
|
||||||
t.Fatalf("short_name byte length = %d", len([]byte(record["short_name"].(string))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildAckServiceEnvelopeRoundTrip(t *testing.T) {
|
|
||||||
key, err := ExpandPSK("AQ==")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExpandPSK: %v", err)
|
|
||||||
}
|
|
||||||
const requestID uint32 = 0xabcd1234
|
|
||||||
raw, err := BuildAckServiceEnvelope(AckBuildOptions{
|
|
||||||
PacketBuildOptions: PacketBuildOptions{
|
|
||||||
FromNodeNum: 0x10101010,
|
|
||||||
ToNodeNum: 0x20202020,
|
|
||||||
PacketID: 0x30303030,
|
|
||||||
ChannelID: "LongFast",
|
|
||||||
GatewayID: "!10101010",
|
|
||||||
PSK: key,
|
|
||||||
Encrypt: true,
|
|
||||||
ViaMQTT: true,
|
|
||||||
},
|
|
||||||
RequestID: requestID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildAckServiceEnvelope: %v", err)
|
|
||||||
}
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!10101010", raw, key, Options{})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP not valid: %#v", record)
|
|
||||||
}
|
|
||||||
if record["portnum"] != "ROUTING_APP" {
|
|
||||||
t.Fatalf("portnum = %v", record["portnum"])
|
|
||||||
}
|
|
||||||
if record["type"] != "routing" {
|
|
||||||
t.Fatalf("type = %v", record["type"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseNodeID(t *testing.T) {
|
|
||||||
num, err := ParseNodeID("!1234abcd")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ParseNodeID() error = %v", err)
|
|
||||||
}
|
|
||||||
if num != 0x1234abcd {
|
|
||||||
t.Fatalf("num = %#x", num)
|
|
||||||
}
|
|
||||||
if NodeNumToID(num) != "!1234abcd" {
|
|
||||||
t.Fatalf("NodeNumToID() = %s", NodeNumToID(num))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package mqtpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMQTTPPEncryptedPacketDefaultRejected(t *testing.T) {
|
|
||||||
raw := encryptedServiceEnvelopeTestPayload()
|
|
||||||
valid, payload, record := MQTTPP("msh/test", raw, nil, Options{})
|
|
||||||
if valid {
|
|
||||||
t.Fatalf("valid = true, want false")
|
|
||||||
}
|
|
||||||
if payload != nil {
|
|
||||||
t.Fatalf("payload = %v, want nil", payload)
|
|
||||||
}
|
|
||||||
if record["type"] != "encrypted_packet" {
|
|
||||||
t.Fatalf("type = %v, want encrypted_packet", record["type"])
|
|
||||||
}
|
|
||||||
if record["error"] != "cannot be decrypted" {
|
|
||||||
t.Fatalf("error = %v, want cannot be decrypted", record["error"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMQTTPPEncryptedPacketAllowed(t *testing.T) {
|
|
||||||
raw := encryptedServiceEnvelopeTestPayload()
|
|
||||||
valid, payload, record := MQTTPP("msh/test", raw, nil, Options{AllowEncryptedForwarding: true})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("valid = false, want true: %+v", record)
|
|
||||||
}
|
|
||||||
if string(payload) != string(raw) {
|
|
||||||
t.Fatalf("payload = %v, want raw payload", payload)
|
|
||||||
}
|
|
||||||
if record["type"] != "encrypted_packet" {
|
|
||||||
t.Fatalf("type = %v, want encrypted_packet", record["type"])
|
|
||||||
}
|
|
||||||
if record["error"] != nil {
|
|
||||||
t.Fatalf("error = %v, want nil", record["error"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptedServiceEnvelopeTestPayload() []byte {
|
|
||||||
packet := protowire.AppendTag(nil, 5, protowire.BytesType)
|
|
||||||
packet = protowire.AppendBytes(packet, []byte{1, 2, 3, 4})
|
|
||||||
envelope := protowire.AppendTag(nil, 1, protowire.BytesType)
|
|
||||||
return protowire.AppendBytes(envelope, packet)
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
package mqtpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdh"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildPKITextMessageRoundTrip(t *testing.T) {
|
|
||||||
curve := ecdh.X25519()
|
|
||||||
senderPriv, err := curve.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generate sender key: %v", err)
|
|
||||||
}
|
|
||||||
recipientPriv, err := curve.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generate recipient key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = "hello over PKI 你好"
|
|
||||||
const fromNum uint32 = 0x12345678
|
|
||||||
const toNum uint32 = 0xa1b2c3d4
|
|
||||||
const packetID uint32 = 0xdeadbeef
|
|
||||||
|
|
||||||
raw, err := BuildPKITextMessageServiceEnvelope(PKITextMessageBuildOptions{
|
|
||||||
FromNodeNum: fromNum,
|
|
||||||
ToNodeNum: toNum,
|
|
||||||
PacketID: packetID,
|
|
||||||
GatewayID: NodeNumToID(fromNum),
|
|
||||||
ViaMQTT: true,
|
|
||||||
SenderPrivate: senderPriv.Bytes(),
|
|
||||||
RecipientPub: recipientPriv.PublicKey().Bytes(),
|
|
||||||
SenderPublic: senderPriv.PublicKey().Bytes(),
|
|
||||||
Text: text,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildPKITextMessageServiceEnvelope: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env, err := parseServiceEnvelope(raw)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parseServiceEnvelope: %v", err)
|
|
||||||
}
|
|
||||||
if env.ChannelID != PKIChannelID {
|
|
||||||
t.Fatalf("channel_id = %q want %q", env.ChannelID, PKIChannelID)
|
|
||||||
}
|
|
||||||
if env.GatewayID != NodeNumToID(fromNum) {
|
|
||||||
t.Fatalf("gateway_id = %q", env.GatewayID)
|
|
||||||
}
|
|
||||||
pkt := env.Packet
|
|
||||||
if pkt.From != fromNum || pkt.To != toNum || pkt.ID != packetID {
|
|
||||||
t.Fatalf("packet header mismatch: %+v", pkt)
|
|
||||||
}
|
|
||||||
if !pkt.PKIEncrypted {
|
|
||||||
t.Fatalf("pki_encrypted = false")
|
|
||||||
}
|
|
||||||
if !pkt.ViaMQTT {
|
|
||||||
t.Fatalf("via_mqtt = false")
|
|
||||||
}
|
|
||||||
if pkt.Channel != 0 {
|
|
||||||
t.Fatalf("channel = %d want 0", pkt.Channel)
|
|
||||||
}
|
|
||||||
if pkt.PayloadVariant != "encrypted" || len(pkt.Encrypted) <= pkcOverhead {
|
|
||||||
t.Fatalf("encrypted payload missing: %+v", pkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收件人用对端私钥 + 发件人公钥推导共享密钥并解密
|
|
||||||
sharedKey, err := pkiSharedKey(recipientPriv.Bytes(), senderPriv.PublicKey().Bytes())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("pkiSharedKey: %v", err)
|
|
||||||
}
|
|
||||||
encryptedLen := len(pkt.Encrypted) - pkcOverhead
|
|
||||||
ciphertext := pkt.Encrypted[:encryptedLen]
|
|
||||||
auth := pkt.Encrypted[encryptedLen : encryptedLen+8]
|
|
||||||
extraNonce := binary.LittleEndian.Uint32(pkt.Encrypted[encryptedLen+8:])
|
|
||||||
plaintext, err := aesCCMDecrypt(sharedKey, pkiNonce(packetID, fromNum, extraNonce), ciphertext, auth)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("aesCCMDecrypt: %v", err)
|
|
||||||
}
|
|
||||||
data, err := parseDataPacket(plaintext)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parseDataPacket: %v", err)
|
|
||||||
}
|
|
||||||
if data.Portnum != textMessageApp {
|
|
||||||
t.Fatalf("portnum = %d", data.Portnum)
|
|
||||||
}
|
|
||||||
if string(data.Payload) != text {
|
|
||||||
t.Fatalf("text = %q want %q", string(data.Payload), text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同样用 MQTTPP 解析路径:PKI 包对外应被识别为 encrypted_packet(无法解密),
|
|
||||||
// 但用错的 PSK 不应误报“channel hash mismatch” 之外的奇怪错误。
|
|
||||||
dummyPSK, _ := ExpandPSK("AQ==")
|
|
||||||
_, _, record := MQTTPP("msh/2/e/PKI/!12345678", raw, dummyPSK, Options{AllowEncryptedForwarding: true})
|
|
||||||
if record["channel_id"] != PKIChannelID {
|
|
||||||
t.Fatalf("MQTTPP record channel_id = %v", record["channel_id"])
|
|
||||||
}
|
|
||||||
if record["pki_encrypted"] != true {
|
|
||||||
t.Fatalf("pki_encrypted record = %v", record["pki_encrypted"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPKINonceLayoutMatchesFirmware(t *testing.T) {
|
|
||||||
// 复刻 firmware initNonce(fromNode, packetId, extraNonce) 期望的字节布局:
|
|
||||||
// nonce[0..8) = packetId(uint64 LE)
|
|
||||||
// nonce[4..8) 被 extraNonce(uint32 LE) 覆盖(当 extraNonce != 0)
|
|
||||||
// nonce[8..12) = fromNode(uint32 LE)
|
|
||||||
// nonce[12] = 0
|
|
||||||
got := pkiNonce(0xaabbccdd, 0x11223344, 0x55667788)
|
|
||||||
want := []byte{
|
|
||||||
0xdd, 0xcc, 0xbb, 0xaa, // packetId low 4 bytes,未被 extraNonce 覆盖前
|
|
||||||
0x88, 0x77, 0x66, 0x55, // extraNonce 覆盖 nonce[4..8)
|
|
||||||
0x44, 0x33, 0x22, 0x11, // fromNode
|
|
||||||
0x00,
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, want) {
|
|
||||||
t.Fatalf("pkiNonce = % x\nwant % x", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildPKITextMessageRejectsBroadcast(t *testing.T) {
|
|
||||||
curve := ecdh.X25519()
|
|
||||||
priv, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
pub, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
if _, err := BuildPKITextMessageServiceEnvelope(PKITextMessageBuildOptions{
|
|
||||||
FromNodeNum: 0x1,
|
|
||||||
ToNodeNum: NodeNumBroadcast,
|
|
||||||
PacketID: 0x2,
|
|
||||||
SenderPrivate: priv.Bytes(),
|
|
||||||
RecipientPub: pub.PublicKey().Bytes(),
|
|
||||||
Text: "hi",
|
|
||||||
}); err == nil {
|
|
||||||
t.Fatalf("expected error for broadcast destination")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认 MeshPacket 中确实带上 pki_encrypted (tag 17) 与 public_key (tag 16)
|
|
||||||
func TestBuildPKIMeshPacketTags(t *testing.T) {
|
|
||||||
encrypted := []byte{0x01, 0x02, 0x03}
|
|
||||||
pub := make([]byte, 32)
|
|
||||||
for i := range pub {
|
|
||||||
pub[i] = byte(i)
|
|
||||||
}
|
|
||||||
raw := buildPKIMeshPacket(0x11, 0x22, 0x33, true, encrypted, pub)
|
|
||||||
tags := map[protowire.Number]bool{}
|
|
||||||
if err := walkFields(raw, func(num protowire.Number, _ protowire.Type, _ any) error {
|
|
||||||
tags[num] = true
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("walkFields: %v", err)
|
|
||||||
}
|
|
||||||
for _, want := range []protowire.Number{1, 2, 5, 6, 14, 16, 17} {
|
|
||||||
if !tags[want] {
|
|
||||||
t.Fatalf("missing tag %d", want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 端到端:发送方构造 PKI 包,接收方通过 PKIKeyResolver 解密并还原文本消息记录。
|
|
||||||
func TestMQTTPPDecryptsPKIWithResolver(t *testing.T) {
|
|
||||||
curve := ecdh.X25519()
|
|
||||||
senderPriv, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
recipientPriv, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
|
|
||||||
const text = "hello PKI inbound"
|
|
||||||
const fromNum uint32 = 0xaaaa1111
|
|
||||||
const toNum uint32 = 0xbbbb2222
|
|
||||||
const packetID uint32 = 0x77777777
|
|
||||||
|
|
||||||
raw, err := BuildPKITextMessageServiceEnvelope(PKITextMessageBuildOptions{
|
|
||||||
FromNodeNum: fromNum,
|
|
||||||
ToNodeNum: toNum,
|
|
||||||
PacketID: packetID,
|
|
||||||
GatewayID: NodeNumToID(fromNum),
|
|
||||||
ViaMQTT: true,
|
|
||||||
SenderPrivate: senderPriv.Bytes(),
|
|
||||||
RecipientPub: recipientPriv.PublicKey().Bytes(),
|
|
||||||
SenderPublic: senderPriv.PublicKey().Bytes(),
|
|
||||||
Text: text,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("build: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver := func(to, from uint32) ([]byte, []byte, bool) {
|
|
||||||
if to != toNum || from != fromNum {
|
|
||||||
return nil, nil, false
|
|
||||||
}
|
|
||||||
return recipientPriv.Bytes(), senderPriv.PublicKey().Bytes(), true
|
|
||||||
}
|
|
||||||
dummyPSK, _ := ExpandPSK("AQ==")
|
|
||||||
valid, _, record := MQTTPP("msh/2/e/PKI/!aaaa1111", raw, dummyPSK, Options{PKIKeyResolver: resolver})
|
|
||||||
if !valid {
|
|
||||||
t.Fatalf("MQTTPP not valid: %#v", record)
|
|
||||||
}
|
|
||||||
if record["type"] != "text_message" {
|
|
||||||
t.Fatalf("type = %v, want text_message", record["type"])
|
|
||||||
}
|
|
||||||
if record["text"] != text {
|
|
||||||
t.Fatalf("text = %v", record["text"])
|
|
||||||
}
|
|
||||||
if record["pki_encrypted"] != true {
|
|
||||||
t.Fatalf("pki_encrypted = %v", record["pki_encrypted"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildPKIAckRoundTrip(t *testing.T) {
|
|
||||||
curve := ecdh.X25519()
|
|
||||||
botPriv, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
devicePriv, _ := curve.GenerateKey(rand.Reader)
|
|
||||||
|
|
||||||
const fromNum uint32 = 0x0000beef // bot
|
|
||||||
const toNum uint32 = 0xfeed0000 // 原 device
|
|
||||||
const ackPacketID uint32 = 0xaaaa5555
|
|
||||||
const requestID uint32 = 0xdeadbeef
|
|
||||||
|
|
||||||
raw, err := BuildPKIAckServiceEnvelope(PKIAckBuildOptions{
|
|
||||||
FromNodeNum: fromNum,
|
|
||||||
ToNodeNum: toNum,
|
|
||||||
PacketID: ackPacketID,
|
|
||||||
RequestID: requestID,
|
|
||||||
GatewayID: NodeNumToID(fromNum),
|
|
||||||
ViaMQTT: true,
|
|
||||||
SenderPrivate: botPriv.Bytes(),
|
|
||||||
RecipientPub: devicePriv.PublicKey().Bytes(),
|
|
||||||
SenderPublic: botPriv.PublicKey().Bytes(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildPKIAckServiceEnvelope: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备侧解密
|
|
||||||
env, err := parseServiceEnvelope(raw)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse: %v", err)
|
|
||||||
}
|
|
||||||
if env.ChannelID != PKIChannelID {
|
|
||||||
t.Fatalf("channel_id = %q", env.ChannelID)
|
|
||||||
}
|
|
||||||
pkt := env.Packet
|
|
||||||
if !pkt.PKIEncrypted || pkt.From != fromNum || pkt.To != toNum || pkt.ID != ackPacketID {
|
|
||||||
t.Fatalf("ack header mismatch: %+v", pkt)
|
|
||||||
}
|
|
||||||
encryptedLen := len(pkt.Encrypted) - pkcOverhead
|
|
||||||
cipher := pkt.Encrypted[:encryptedLen]
|
|
||||||
auth := pkt.Encrypted[encryptedLen : encryptedLen+8]
|
|
||||||
extraNonce := binary.LittleEndian.Uint32(pkt.Encrypted[encryptedLen+8:])
|
|
||||||
sharedKey, err := pkiSharedKey(devicePriv.Bytes(), botPriv.PublicKey().Bytes())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("shared: %v", err)
|
|
||||||
}
|
|
||||||
plain, err := aesCCMDecrypt(sharedKey, pkiNonce(ackPacketID, fromNum, extraNonce), cipher, auth)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decrypt: %v", err)
|
|
||||||
}
|
|
||||||
data, err := parseDataPacket(plain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("data: %v", err)
|
|
||||||
}
|
|
||||||
if data.Portnum != routingApp {
|
|
||||||
t.Fatalf("portnum = %d, want ROUTING_APP(%d)", data.Portnum, routingApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Routing payload 解析: 期望 oneof error_reason=NONE(0),即 wire 字节 0x18 0x00
|
|
||||||
wantRouting := []byte{0x18, 0x00}
|
|
||||||
if !bytes.Equal(data.Payload, wantRouting) {
|
|
||||||
t.Fatalf("routing payload = % x, want % x", data.Payload, wantRouting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package runtimesettings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
storepkg "meshtastic_mqtt_server/internal/store"
|
|
||||||
"meshtastic_mqtt_server/internal/store/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func openTestStore(t *testing.T) *storepkg.Store {
|
|
||||||
return testutil.OpenStore(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRuntimeSettingsCacheReload(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
cache, err := New(st)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("New() error = %v", err)
|
|
||||||
}
|
|
||||||
if cache.AllowEncryptedForwarding() {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding() = true, want false")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.SetBoolRuntimeSetting(storepkg.RuntimeSettingAllowEncryptedForwarding, true, "test setting"); err != nil {
|
|
||||||
t.Fatalf("SetBoolRuntimeSetting(true) error = %v", err)
|
|
||||||
}
|
|
||||||
if err := cache.Reload(st); err != nil {
|
|
||||||
t.Fatalf("Reload() after true error = %v", err)
|
|
||||||
}
|
|
||||||
if !cache.AllowEncryptedForwarding() {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding() = false, want true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.SetBoolRuntimeSetting(storepkg.RuntimeSettingAllowEncryptedForwarding, false, "test setting"); err != nil {
|
|
||||||
t.Fatalf("SetBoolRuntimeSetting(false) error = %v", err)
|
|
||||||
}
|
|
||||||
if err := cache.Reload(st); err != nil {
|
|
||||||
t.Fatalf("Reload() after false error = %v", err)
|
|
||||||
}
|
|
||||||
if cache.AllowEncryptedForwarding() {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding() = true, want false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNodeBlockingCRUD(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
nodeNum := int64(305419896)
|
|
||||||
rule, err := st.CreateNodeBlocking(" !12345678 ", &nodeNum, " noisy node ", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateNodeBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if rule.NodeID != "!12345678" || rule.NodeNum == nil || *rule.NodeNum != nodeNum || rule.Reason != "noisy node" || !rule.Enabled {
|
|
||||||
t.Fatalf("created node rule = %+v, want normalized fields", rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.CreateNodeBlocking("!12345678", nil, "duplicate", true); !errors.Is(err, ErrBlockingAlreadyExists) {
|
|
||||||
t.Fatalf("duplicate CreateNodeBlocking() error = %v, want ErrBlockingAlreadyExists", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedNum := int64(7)
|
|
||||||
updated, err := st.UpdateNodeBlocking(rule.ID, "!00000007", &updatedNum, "updated", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UpdateNodeBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if updated.NodeID != "!00000007" || updated.NodeNum == nil || *updated.NodeNum != updatedNum || updated.Reason != "updated" || updated.Enabled {
|
|
||||||
t.Fatalf("updated node rule = %+v, want updated fields", updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := st.ListNodeBlocking(ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListNodeBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(rows) != 1 || rows[0].ID != rule.ID {
|
|
||||||
t.Fatalf("ListNodeBlocking() = %+v, want one updated rule", rows)
|
|
||||||
}
|
|
||||||
total, err := st.CountNodeBlocking(ListOptions{})
|
|
||||||
if err != nil || total != 1 {
|
|
||||||
t.Fatalf("CountNodeBlocking() = %d, %v, want 1, nil", total, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.DeleteNodeBlocking(rule.ID); err != nil {
|
|
||||||
t.Fatalf("DeleteNodeBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if err := st.DeleteNodeBlocking(rule.ID); !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("DeleteNodeBlocking(missing) error = %v, want record not found", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeBlockingValidation(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
if _, err := st.CreateNodeBlocking(" ", nil, "", true); err == nil {
|
|
||||||
t.Fatal("CreateNodeBlocking(empty) error = nil, want error")
|
|
||||||
}
|
|
||||||
if _, err := st.UpdateNodeBlocking(1, "!missing", nil, "", true); !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("UpdateNodeBlocking(missing) error = %v, want record not found", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPBlockingCRUDAndValidation(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
rule, err := st.CreateIPBlocking(" 127.0.0.1 ", "local", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateIPBlocking(ip) error = %v", err)
|
|
||||||
}
|
|
||||||
if rule.IPValue != "127.0.0.1" || rule.Reason != "local" || !rule.Enabled {
|
|
||||||
t.Fatalf("created ip rule = %+v, want normalized IP", rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
cidr, err := st.CreateIPBlocking("192.168.1.99/24", "cidr", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateIPBlocking(cidr) error = %v", err)
|
|
||||||
}
|
|
||||||
if cidr.IPValue != "192.168.1.0/24" {
|
|
||||||
t.Fatalf("cidr IPValue = %q, want 192.168.1.0/24", cidr.IPValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.CreateIPBlocking("127.0.0.1", "duplicate", true); !errors.Is(err, ErrBlockingAlreadyExists) {
|
|
||||||
t.Fatalf("duplicate CreateIPBlocking() error = %v, want ErrBlockingAlreadyExists", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateIPBlocking("not-an-ip", "invalid", true); err == nil {
|
|
||||||
t.Fatal("CreateIPBlocking(invalid) error = nil, want error")
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := st.UpdateIPBlocking(rule.ID, "10.0.0.0/8", "updated", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UpdateIPBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if updated.IPValue != "10.0.0.0/8" || updated.Reason != "updated" || updated.Enabled {
|
|
||||||
t.Fatalf("updated ip rule = %+v, want updated fields", updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := st.ListIPBlocking(ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListIPBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(rows) != 2 {
|
|
||||||
t.Fatalf("ListIPBlocking() length = %d, want 2", len(rows))
|
|
||||||
}
|
|
||||||
total, err := st.CountIPBlocking(ListOptions{})
|
|
||||||
if err != nil || total != 2 {
|
|
||||||
t.Fatalf("CountIPBlocking() = %d, %v, want 2, nil", total, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.DeleteIPBlocking(rule.ID); err != nil {
|
|
||||||
t.Fatalf("DeleteIPBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if err := st.DeleteIPBlocking(rule.ID); !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("DeleteIPBlocking(missing) error = %v, want record not found", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListEnabledBlockingRules(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
nodeNum := int64(1)
|
|
||||||
if _, err := st.CreateNodeBlocking("!00000001", &nodeNum, "enabled", true); err != nil {
|
|
||||||
t.Fatalf("CreateNodeBlocking(enabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateNodeBlocking("!00000002", nil, "disabled", false); err != nil {
|
|
||||||
t.Fatalf("CreateNodeBlocking(disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if rows, err := st.ListEnabledNodeBlocking(); err != nil || len(rows) != 1 || rows[0].NodeID != "!00000001" {
|
|
||||||
t.Fatalf("ListEnabledNodeBlocking() = %+v, %v, want only enabled node", rows, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.CreateIPBlocking("127.0.0.1", "enabled", true); err != nil {
|
|
||||||
t.Fatalf("CreateIPBlocking(enabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateIPBlocking("192.168.1.1", "disabled", false); err != nil {
|
|
||||||
t.Fatalf("CreateIPBlocking(disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if rows, err := st.ListEnabledIPBlocking(); err != nil || len(rows) != 1 || rows[0].IPValue != "127.0.0.1" {
|
|
||||||
t.Fatalf("ListEnabledIPBlocking() = %+v, %v, want only enabled IP", rows, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("spam", "contains", false, "enabled", true); err != nil {
|
|
||||||
t.Fatalf("CreateForbiddenWordBlocking(enabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("eggs", "contains", false, "disabled", false); err != nil {
|
|
||||||
t.Fatalf("CreateForbiddenWordBlocking(disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
if rows, err := st.ListEnabledForbiddenWordBlocking(); err != nil || len(rows) != 1 || rows[0].Word != "spam" {
|
|
||||||
t.Fatalf("ListEnabledForbiddenWordBlocking() = %+v, %v, want only enabled word", rows, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForbiddenWordBlockingCRUDAndValidation(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
rule, err := st.CreateForbiddenWordBlocking(" spam ", "", false, "junk", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if rule.Word != "spam" || rule.MatchType != ForbiddenWordMatchContains || rule.CaseSensitive || rule.Reason != "junk" || !rule.Enabled {
|
|
||||||
t.Fatalf("created word rule = %+v, want normalized fields", rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("spam", "contains", false, "duplicate", true); !errors.Is(err, ErrBlockingAlreadyExists) {
|
|
||||||
t.Fatalf("duplicate CreateForbiddenWordBlocking() error = %v, want ErrBlockingAlreadyExists", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking(" ", "contains", false, "empty", true); err == nil {
|
|
||||||
t.Fatal("CreateForbiddenWordBlocking(empty) error = nil, want error")
|
|
||||||
}
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("regex", "regex", false, "unsupported", true); err == nil {
|
|
||||||
t.Fatal("CreateForbiddenWordBlocking(unsupported match type) error = nil, want error")
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := st.UpdateForbiddenWordBlocking(rule.ID, "Spam", "contains", true, "updated", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UpdateForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if updated.Word != "Spam" || updated.MatchType != "contains" || !updated.CaseSensitive || updated.Reason != "updated" || updated.Enabled {
|
|
||||||
t.Fatalf("updated word rule = %+v, want updated fields", updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := st.ListForbiddenWordBlocking(ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(rows) != 1 || rows[0].ID != rule.ID {
|
|
||||||
t.Fatalf("ListForbiddenWordBlocking() = %+v, want one updated rule", rows)
|
|
||||||
}
|
|
||||||
total, err := st.CountForbiddenWordBlocking(ListOptions{})
|
|
||||||
if err != nil || total != 1 {
|
|
||||||
t.Fatalf("CountForbiddenWordBlocking() = %d, %v, want 1, nil", total, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.DeleteForbiddenWordBlocking(rule.ID); err != nil {
|
|
||||||
t.Fatalf("DeleteForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
if err := st.DeleteForbiddenWordBlocking(rule.ID); !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("DeleteForbiddenWordBlocking(missing) error = %v, want record not found", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,104 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDBWriteQueueWritesRecordsAsync(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
queue := newDBWriteQueue(st)
|
|
||||||
record := textMessageTestRecord("queued")
|
|
||||||
queue.EnqueueRecord(record, MQTTClientInfo{ClientID: "client-1"})
|
|
||||||
record["text"] = "mutated after enqueue"
|
|
||||||
queue.Close()
|
|
||||||
|
|
||||||
var text, clientID string
|
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT text, mqtt_client_id FROM text_message WHERE from_id = ?", "!12345678").Scan(&text, &clientID); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if text != "queued" || clientID != "client-1" {
|
|
||||||
t.Fatalf("queued row = text %q client %q, want queued/client-1", text, clientID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBWriteQueueWritesDiscardAsync(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
queue := newDBWriteQueue(st)
|
|
||||||
record := map[string]any{"topic": "msh/test", "error": "bad packet"}
|
|
||||||
queue.EnqueueDiscard(record, []byte{1, 2, 3}, MQTTClientInfo{RemoteAddr: "127.0.0.1:1883"})
|
|
||||||
record["error"] = "mutated after enqueue"
|
|
||||||
queue.Close()
|
|
||||||
|
|
||||||
var topic, reason, rawBase64, remoteAddr string
|
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT topic, error, raw_base64, mqtt_remote_addr FROM discard_details").Scan(&topic, &reason, &rawBase64, &remoteAddr); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if topic != "msh/test" || reason != "bad packet" || rawBase64 != "AQID" || remoteAddr != "127.0.0.1:1883" {
|
|
||||||
t.Fatalf("discard row = %q/%q/%q/%q, want queued values", topic, reason, rawBase64, remoteAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBWriteQueueLen(t *testing.T) {
|
|
||||||
queue := &WriteQueue{jobs: make(chan writeJob, 1)}
|
|
||||||
queue.enqueue(writeJob{run: func() error { return nil }})
|
|
||||||
if queue.Len() != 1 {
|
|
||||||
t.Fatalf("queue.Len() = %d, want 1", queue.Len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBWriteQueueIgnoresUnsupportedRecordType(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
queue := newDBWriteQueue(st)
|
|
||||||
queue.EnqueueRecord(map[string]any{"type": "empty_packet", "from": "!12345678"}, MQTTClientInfo{})
|
|
||||||
queue.Close()
|
|
||||||
|
|
||||||
var count int
|
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM text_message").Scan(&count); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatalf("text_message count = %d, want 0", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBWriteQueueNilStore(t *testing.T) {
|
|
||||||
if queue := newDBWriteQueue(nil); queue != nil {
|
|
||||||
t.Fatalf("newDBWriteQueue(nil) = %#v, want nil", queue)
|
|
||||||
}
|
|
||||||
var queue *WriteQueue
|
|
||||||
queue.EnqueueRecord(textMessageTestRecord("ignored"), MQTTClientInfo{})
|
|
||||||
queue.EnqueueDiscard(map[string]any{"topic": "ignored"}, []byte{1}, MQTTClientInfo{})
|
|
||||||
queue.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBWriteQueueRecordValidationErrorDoesNotStopWorker(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
queue := newDBWriteQueue(st)
|
|
||||||
badRecord := textMessageTestRecord("bad")
|
|
||||||
delete(badRecord, "from")
|
|
||||||
queue.EnqueueRecord(badRecord, MQTTClientInfo{})
|
|
||||||
queue.EnqueueRecord(textMessageTestRecord("good"), MQTTClientInfo{})
|
|
||||||
queue.Close()
|
|
||||||
|
|
||||||
var text string
|
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT text FROM text_message").Scan(&text); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if text != "good" {
|
|
||||||
t.Fatalf("text = %q, want good", text)
|
|
||||||
}
|
|
||||||
|
|
||||||
var missing sql.NullString
|
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT text FROM text_message WHERE text = ?", "bad").Scan(&missing); err != sql.ErrNoRows {
|
|
||||||
t.Fatalf("bad row error = %v, want sql.ErrNoRows", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMapTileSourceDefaultSeeded(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.GetDefaultMapTileSource()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetDefaultMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
if row.Name != defaultMapTileSourceName || row.URLTemplate != defaultMapTileSourceURLTemplate || !row.Enabled || !row.IsDefault {
|
|
||||||
t.Fatalf("default map source = %+v, want built-in default", row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateMapTileSourceValidation(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
if _, err := st.CreateMapTileSource(MapTileSourceInput{Name: "bad", URLTemplate: "https://tiles.example.com/{z}/{x}.png", MaxZoom: 19, Enabled: true, ProxyEnabled: true}); err == nil {
|
|
||||||
t.Fatal("CreateMapTileSource() missing placeholder error = nil, want error")
|
|
||||||
}
|
|
||||||
if _, err := st.CreateMapTileSource(MapTileSourceInput{Name: "bad", URLTemplate: "javascript:alert(1)/{z}/{x}/{y}", MaxZoom: 19, Enabled: true, ProxyEnabled: true}); err == nil {
|
|
||||||
t.Fatal("CreateMapTileSource() invalid scheme error = nil, want error")
|
|
||||||
}
|
|
||||||
if _, err := st.CreateMapTileSource(MapTileSourceInput{Name: "bad", URLTemplate: "https://user:pass@tiles.example.com/{z}/{x}/{y}.png", MaxZoom: 19, Enabled: true, ProxyEnabled: true}); err == nil {
|
|
||||||
t.Fatal("CreateMapTileSource() credentials error = nil, want error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListEnabledMapTileSources(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
disabled, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Disabled", URLTemplate: "https://disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
custom, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Custom", URLTemplate: "https://custom.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(custom) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := st.SetDefaultMapTileSource(custom.ID); err != nil {
|
|
||||||
t.Fatalf("SetDefaultMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := st.ListEnabledMapTileSources()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListEnabledMapTileSources() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(rows) < 2 {
|
|
||||||
t.Fatalf("ListEnabledMapTileSources() length = %d, want at least 2", len(rows))
|
|
||||||
}
|
|
||||||
if rows[0].ID != custom.ID {
|
|
||||||
t.Fatalf("first enabled source id = %d, want default %d", rows[0].ID, custom.ID)
|
|
||||||
}
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.ID == disabled.ID {
|
|
||||||
t.Fatalf("disabled source was returned: %+v", row)
|
|
||||||
}
|
|
||||||
if !row.Enabled {
|
|
||||||
t.Fatalf("disabled row returned: %+v", row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileSourceDuplicateAndDefaultRules(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
first, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Custom", URLTemplate: "https://tiles.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Custom", URLTemplate: "https://tiles2.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true}); !errors.Is(err, ErrMapTileSourceAlreadyExists) {
|
|
||||||
t.Fatalf("duplicate name error = %v, want ErrMapTileSourceAlreadyExists", err)
|
|
||||||
}
|
|
||||||
if _, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Custom 2", URLTemplate: first.URLTemplate, MaxZoom: 18, Enabled: true, ProxyEnabled: true}); !errors.Is(err, ErrMapTileSourceAlreadyExists) {
|
|
||||||
t.Fatalf("duplicate url error = %v, want ErrMapTileSourceAlreadyExists", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := st.SetDefaultMapTileSource(first.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SetDefaultMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
if !updated.IsDefault {
|
|
||||||
t.Fatalf("updated default = %+v, want is_default", updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldDefault, err := st.GetDefaultMapTileSource()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetDefaultMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
if oldDefault.ID != first.ID {
|
|
||||||
t.Fatalf("default id = %d, want %d", oldDefault.ID, first.ID)
|
|
||||||
}
|
|
||||||
if _, err := st.UpdateMapTileSource(first.ID, MapTileSourceInput{Name: first.Name, URLTemplate: first.URLTemplate, Attribution: first.Attribution, MaxZoom: first.MaxZoom, Enabled: false, IsDefault: true}); !errors.Is(err, ErrMapTileSourceCannotDisableDefault) {
|
|
||||||
t.Fatalf("disable default error = %v, want ErrMapTileSourceCannotDisableDefault", err)
|
|
||||||
}
|
|
||||||
if err := st.DeleteMapTileSource(first.ID); !errors.Is(err, ErrMapTileSourceCannotDeleteDefault) {
|
|
||||||
t.Fatalf("delete default error = %v, want ErrMapTileSourceCannotDeleteDefault", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileSourceHashIsSetOnCreate(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "Hashed", URLTemplate: "https://test.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
want := MapTileSourceHash("https://test.example.com/{z}/{x}/{y}.png")
|
|
||||||
if row.URLTemplateHash != want {
|
|
||||||
t.Fatalf("URLTemplateHash = %q, want %q", row.URLTemplateHash, want)
|
|
||||||
}
|
|
||||||
if !row.ProxyEnabled {
|
|
||||||
t.Fatal("ProxyEnabled = false, want true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileSourceDefaultHasHash(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.GetDefaultMapTileSource()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetDefaultMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
want := MapTileSourceHash(defaultMapTileSourceURLTemplate)
|
|
||||||
if row.URLTemplateHash != want {
|
|
||||||
t.Fatalf("default URLTemplateHash = %q, want %q", row.URLTemplateHash, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEnabledMapTileSourceByHash(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "HashLookup", URLTemplate: "https://lookup.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found, err := st.GetEnabledMapTileSourceByHash(row.URLTemplateHash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetEnabledMapTileSourceByHash() error = %v", err)
|
|
||||||
}
|
|
||||||
if found.ID != row.ID {
|
|
||||||
t.Fatalf("found ID = %d, want %d", found.ID, row.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEnabledMapTileSourceByHashDisabled(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "DisabledHash", URLTemplate: "https://disabled-hash.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = st.GetEnabledMapTileSourceByHash(row.URLTemplateHash)
|
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("GetEnabledMapTileSourceByHash(disabled) = %v, want gorm.ErrRecordNotFound", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEnabledMapTileSourceByHashProxyDisabled(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "ProxyDisabledHash", URLTemplate: "https://proxy-disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = st.GetEnabledMapTileSourceByHash(row.URLTemplateHash)
|
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("GetEnabledMapTileSourceByHash(proxy disabled) = %v, want gorm.ErrRecordNotFound", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEnabledMapTileSourceByHashUnknown(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
_, err := st.GetEnabledMapTileSourceByHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
t.Fatalf("GetEnabledMapTileSourceByHash(unknown) = %v, want gorm.ErrRecordNotFound", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublicMapTileSourceDTOProxyURL(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "ProxyTest", URLTemplate: "https://proxy.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dto := publicMapTileSourceDTO(*row)
|
|
||||||
urlTemplate, ok := dto["url_template"].(string)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("url_template is not a string")
|
|
||||||
}
|
|
||||||
wantPrefix := "/api/map/" + row.URLTemplateHash + "?x={x}&y={y}&z={z}"
|
|
||||||
if urlTemplate != wantPrefix {
|
|
||||||
t.Fatalf("url_template = %q, want %q", urlTemplate, wantPrefix)
|
|
||||||
}
|
|
||||||
if strings.Contains(urlTemplate, "proxy.example.com") {
|
|
||||||
t.Fatal("url_template should not contain upstream hostname")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublicMapTileSourceDTORawURLWhenProxyDisabled(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(MapTileSourceInput{Name: "RawTest", URLTemplate: "https://raw.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dto := publicMapTileSourceDTO(*row)
|
|
||||||
urlTemplate, ok := dto["url_template"].(string)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("url_template is not a string")
|
|
||||||
}
|
|
||||||
if urlTemplate != row.URLTemplate {
|
|
||||||
t.Fatalf("url_template = %q, want raw %q", urlTemplate, row.URLTemplate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileSourceHashFunction(t *testing.T) {
|
|
||||||
hash1 := MapTileSourceHash("https://tile.openstreetmap.jp/{z}/{x}/{y}.png")
|
|
||||||
hash2 := MapTileSourceHash("https://tile.openstreetmap.jp/{z}/{x}/{y}.png")
|
|
||||||
hash3 := MapTileSourceHash("https://other.example.com/{z}/{x}/{y}.png")
|
|
||||||
|
|
||||||
if hash1 != hash2 {
|
|
||||||
t.Fatal("hash should be deterministic")
|
|
||||||
}
|
|
||||||
if len(hash1) != 64 {
|
|
||||||
t.Fatalf("hash length = %d, want 64", len(hash1))
|
|
||||||
}
|
|
||||||
if hash1 == hash3 {
|
|
||||||
t.Fatal("different URLs should produce different hashes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestRuntimeSettingsDefaultAndUpdates(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
settings, err := st.GetRuntimeSettings()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetRuntimeSettings() error = %v", err)
|
|
||||||
}
|
|
||||||
if settings.AllowEncryptedForwarding {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding = true, want false")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.SetBoolRuntimeSetting(RuntimeSettingAllowEncryptedForwarding, true, "test setting"); err != nil {
|
|
||||||
t.Fatalf("SetBoolRuntimeSetting(true) error = %v", err)
|
|
||||||
}
|
|
||||||
settings, err = st.GetRuntimeSettings()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetRuntimeSettings() after true error = %v", err)
|
|
||||||
}
|
|
||||||
if !settings.AllowEncryptedForwarding {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding = false, want true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := st.SetBoolRuntimeSetting(RuntimeSettingAllowEncryptedForwarding, false, "test setting"); err != nil {
|
|
||||||
t.Fatalf("SetBoolRuntimeSetting(false) error = %v", err)
|
|
||||||
}
|
|
||||||
settings, err = st.GetRuntimeSettings()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetRuntimeSettings() after false error = %v", err)
|
|
||||||
}
|
|
||||||
if settings.AllowEncryptedForwarding {
|
|
||||||
t.Fatalf("AllowEncryptedForwarding = true, want false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 测试 helper —— 为从 main 包搬过来的测试提供它们原本依赖的小写函数。
|
|
||||||
// 这些 helper 不暴露给生产代码使用;它们的行为应当与 main 包对应实现保持一致。
|
|
||||||
|
|
||||||
// verifyPassword 复刻 auth.go 中的 bcrypt 校验,用于 user_store 的测试。
|
|
||||||
func verifyPassword(hash, password string) bool {
|
|
||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// publicMapTileSourceDTO 复刻 admin_map_source_routes.go 中的同名函数,
|
|
||||||
// 仅供 map_source_store_test.go 验证 ProxyEnabled 时 URL 是否被改写。
|
|
||||||
// 这里返回 map[string]any 而非 gin.H 以避免引入 gin 依赖。
|
|
||||||
func publicMapTileSourceDTO(row MapTileSourceRecord) map[string]any {
|
|
||||||
urlTemplate := row.URLTemplate
|
|
||||||
if row.ProxyEnabled {
|
|
||||||
hash := row.URLTemplateHash
|
|
||||||
if hash == "" {
|
|
||||||
hash = MapTileSourceHash(row.URLTemplate)
|
|
||||||
}
|
|
||||||
urlTemplate = "/api/map/" + hash + "?x={x}&y={y}&z={z}"
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"id": row.ID,
|
|
||||||
"name": row.Name,
|
|
||||||
"url_template": urlTemplate,
|
|
||||||
"attribution": row.Attribution,
|
|
||||||
"max_zoom": row.MaxZoom,
|
|
||||||
"enabled": row.Enabled,
|
|
||||||
"is_default": row.IsDefault,
|
|
||||||
"proxy_enabled": row.ProxyEnabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDBWriteQueue 是 db_write_queue_test.go 期望的旧名字。重新导出供测试使用。
|
|
||||||
var newDBWriteQueue = NewWriteQueue
|
|
||||||
|
|
||||||
// 让 strings 不会被 import-but-not-used(如果上面用不到,就算了——保留以应对将来扩展)
|
|
||||||
var _ = strings.TrimSpace
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Package testutil 提供给其它包测试使用的 store 临时实例工厂。
|
|
||||||
//
|
|
||||||
// 重构前 db_test.go 中的 openTestStore helper 被 8+ 个测试文件复用;
|
|
||||||
// 现在抽到这里,让 store 包外的测试也可以零样板地拿到一个临时 SQLite store。
|
|
||||||
package testutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"meshtastic_mqtt_server/internal/config"
|
|
||||||
"meshtastic_mqtt_server/internal/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenStore 返回一个写在 t.TempDir() 中的临时 SQLite store。
|
|
||||||
// 测试结束时调用方需要 defer st.Close()。
|
|
||||||
func OpenStore(t *testing.T) *store.Store {
|
|
||||||
t.Helper()
|
|
||||||
st, err := store.OpenStore(config.DatabaseConfig{
|
|
||||||
Driver: config.DriverSQLite,
|
|
||||||
SQLite: config.SQLiteConfig{Path: filepath.Join(t.TempDir(), "mesh_mqtt_go.db")},
|
|
||||||
}, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("OpenStore() error = %v", err)
|
|
||||||
}
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
configpkg "meshtastic_mqtt_server/internal/config"
|
|
||||||
storepkg "meshtastic_mqtt_server/internal/store"
|
|
||||||
"meshtastic_mqtt_server/internal/store/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func openTestStore(t *testing.T) *storepkg.Store {
|
|
||||||
return testutil.OpenStore(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileProxyFetchesAndCaches(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
requests := 0
|
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
requests++
|
|
||||||
if r.URL.Path != "/3/1/2.png" {
|
|
||||||
t.Fatalf("upstream path = %q, want /3/1/2.png", r.URL.Path)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "image/png")
|
|
||||||
_, _ = w.Write([]byte("tile-data"))
|
|
||||||
}))
|
|
||||||
defer upstream.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Tiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
|
||||||
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: cacheDir}, false, st, nil, nil, nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
url := "/api/map/" + row.URLTemplateHash + "?x=1&y=2&z=3"
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
if recorder.Code != http.StatusOK {
|
|
||||||
t.Fatalf("request %d status = %d, body = %s", i+1, recorder.Code, recorder.Body.String())
|
|
||||||
}
|
|
||||||
if recorder.Body.String() != "tile-data" {
|
|
||||||
t.Fatalf("request %d body = %q, want tile-data", i+1, recorder.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if requests != 1 {
|
|
||||||
t.Fatalf("upstream requests = %d, want 1", requests)
|
|
||||||
}
|
|
||||||
|
|
||||||
cachePath := filepath.Join(cacheDir, row.URLTemplateHash, "3", "1", "2.tile")
|
|
||||||
data, err := os.ReadFile(cachePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read cache file %s: %v", cachePath, err)
|
|
||||||
}
|
|
||||||
if string(data) != "tile-data" {
|
|
||||||
t.Fatalf("cache file = %q, want tile-data", string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileProxyRejectsInvalidCoordinates(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
row, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Tiles", URLTemplate: "https://tiles.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, false, st, nil, nil, nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
cases := []string{
|
|
||||||
"/api/map/" + row.URLTemplateHash + "?y=0&z=0",
|
|
||||||
"/api/map/" + row.URLTemplateHash + "?x=-1&y=0&z=0",
|
|
||||||
"/api/map/" + row.URLTemplateHash + "?x=0&y=0&z=4",
|
|
||||||
"/api/map/" + row.URLTemplateHash + "?x=2&y=0&z=1",
|
|
||||||
}
|
|
||||||
for _, url := range cases {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
if recorder.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("%s status = %d, want 400; body = %s", url, recorder.Code, recorder.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileProxyUnknownAndDisabledSource(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
disabled, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Disabled", URLTemplate: "https://disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
proxyDisabled, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "ProxyDisabled", URLTemplate: "https://proxy-disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: true, ProxyEnabled: false})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(proxy disabled) error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, false, st, nil, nil, nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
cases := []string{
|
|
||||||
"/api/map/not-a-hash?x=0&y=0&z=0",
|
|
||||||
"/api/map/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?x=0&y=0&z=0",
|
|
||||||
"/api/map/" + disabled.URLTemplateHash + "?x=0&y=0&z=0",
|
|
||||||
"/api/map/" + proxyDisabled.URLTemplateHash + "?x=0&y=0&z=0",
|
|
||||||
}
|
|
||||||
wantStatus := []int{http.StatusBadRequest, http.StatusNotFound, http.StatusNotFound, http.StatusNotFound}
|
|
||||||
for i, url := range cases {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
if recorder.Code != wantStatus[i] {
|
|
||||||
t.Fatalf("%s status = %d, want %d; body = %s", url, recorder.Code, wantStatus[i], recorder.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapTileProxyUpstreamStatus(t *testing.T) {
|
|
||||||
st := openTestStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.Contains(r.URL.Path, "/404/") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, "upstream error", http.StatusInternalServerError)
|
|
||||||
}))
|
|
||||||
defer upstream.Close()
|
|
||||||
|
|
||||||
row404, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "NotFoundTiles", URLTemplate: upstream.URL + "/404/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(404) error = %v", err)
|
|
||||||
}
|
|
||||||
row500, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "StatusTiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateMapTileSource(500) error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, false, st, nil, nil, nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
url string
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{url: "/api/map/" + row404.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusNotFound},
|
|
||||||
{url: "/api/map/" + row500.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusBadGateway},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, tc.url, nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
if recorder.Code != tc.want {
|
|
||||||
t.Fatalf("%s status = %d, want %d; body = %s", tc.url, recorder.Code, tc.want, recorder.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
mqtt "github.com/mochi-mqtt/server/v2"
|
|
||||||
|
|
||||||
blockingpkg "meshtastic_mqtt_server/internal/blocking"
|
|
||||||
storepkg "meshtastic_mqtt_server/internal/store"
|
|
||||||
"meshtastic_mqtt_server/internal/store/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMQTTClientInfoFromClientNil(t *testing.T) {
|
|
||||||
info := mqttClientInfoFromClient(nil)
|
|
||||||
if info != (storepkg.MQTTClientInfo{}) {
|
|
||||||
t.Fatalf("info = %#v, want zero value", info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMQTTClientInfoFromClientIPv4(t *testing.T) {
|
|
||||||
info := mqttClientInfoFromClient(&mqtt.Client{
|
|
||||||
ID: "client-1",
|
|
||||||
Properties: mqtt.ClientProperties{Username: []byte("user-1")},
|
|
||||||
Net: mqtt.ClientConnection{Listener: "tcp", Remote: "127.0.0.1:1234"},
|
|
||||||
})
|
|
||||||
|
|
||||||
if info.ClientID != "client-1" || info.Username != "user-1" || info.Listener != "tcp" {
|
|
||||||
t.Fatalf("client fields = %#v", info)
|
|
||||||
}
|
|
||||||
if info.RemoteAddr != "127.0.0.1:1234" || info.RemoteHost != "127.0.0.1" || info.RemotePort != "1234" {
|
|
||||||
t.Fatalf("remote fields = %#v", info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMQTTClientInfoFromClientIPv6(t *testing.T) {
|
|
||||||
info := mqttClientInfoFromClient(&mqtt.Client{Net: mqtt.ClientConnection{Remote: "[::1]:1234"}})
|
|
||||||
if info.RemoteHost != "::1" || info.RemotePort != "1234" {
|
|
||||||
t.Fatalf("remote fields = %#v, want host ::1 and port 1234", info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMQTTClientInfoFromClientUnsplitRemote(t *testing.T) {
|
|
||||||
info := mqttClientInfoFromClient(&mqtt.Client{Net: mqtt.ClientConnection{Remote: "localhost"}})
|
|
||||||
if info.RemoteHost != "localhost" || info.RemotePort != "" {
|
|
||||||
t.Fatalf("remote fields = %#v, want host localhost and empty port", info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockingViolationForRecord 的测试用真实 *Store + blocking.Cache 走完整路径,
|
|
||||||
// 不依赖 cache 的未导出字段。
|
|
||||||
|
|
||||||
func TestBlockingViolationForRecordNode(t *testing.T) {
|
|
||||||
st := testutil.OpenStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
nodeNum := int64(305419896)
|
|
||||||
if _, err := st.CreateNodeBlocking("!12345678", &nodeNum, "blocked", true); err != nil {
|
|
||||||
t.Fatalf("CreateNodeBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
cache, err := blockingpkg.New(st)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("blocking.New() error = %v", err)
|
|
||||||
}
|
|
||||||
record := map[string]any{"type": "position", "from": "!12345678", "from_num": uint32(305419896)}
|
|
||||||
violation := blockingViolationForRecord(cache, record)
|
|
||||||
if violation == nil || violation["blocking_type"] != "node" {
|
|
||||||
t.Fatalf("blockingViolationForRecord() = %#v, want node violation", violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlockingViolationForRecordForbiddenWordFields(t *testing.T) {
|
|
||||||
st := testutil.OpenStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("spam", "contains", false, "blocked", true); err != nil {
|
|
||||||
t.Fatalf("CreateForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
cache, err := blockingpkg.New(st)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("blocking.New() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
record map[string]any
|
|
||||||
field string
|
|
||||||
}{
|
|
||||||
{name: "text", record: map[string]any{"type": "text_message", "from": "!1", "text": "has SPAM"}, field: "text"},
|
|
||||||
{name: "nodeinfo", record: map[string]any{"type": "nodeinfo", "from": "!1", "long_name": "has SPAM"}, field: "long_name"},
|
|
||||||
{name: "map_report", record: map[string]any{"type": "map_report", "from": "!1", "long_name": "has SPAM"}, field: "long_name"},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
violation := blockingViolationForRecord(cache, tc.record)
|
|
||||||
if violation == nil || violation["blocking_type"] != "forbidden_word" || violation["blocking_field"] != tc.field || violation["matched_word"] != "spam" {
|
|
||||||
t.Fatalf("blockingViolationForRecord() = %#v, want forbidden word on %s", violation, tc.field)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlockingViolationForRecordAllowed(t *testing.T) {
|
|
||||||
st := testutil.OpenStore(t)
|
|
||||||
defer st.Close()
|
|
||||||
if _, err := st.CreateForbiddenWordBlocking("spam", "contains", false, "blocked", true); err != nil {
|
|
||||||
t.Fatalf("CreateForbiddenWordBlocking() error = %v", err)
|
|
||||||
}
|
|
||||||
cache, err := blockingpkg.New(st)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("blocking.New() error = %v", err)
|
|
||||||
}
|
|
||||||
record := map[string]any{"type": "text_message", "from": "!1", "text": "hello"}
|
|
||||||
if violation := blockingViolationForRecord(cache, record); violation != nil {
|
|
||||||
t.Fatalf("blockingViolationForRecord() = %#v, want nil", violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mqtt "github.com/mochi-mqtt/server/v2"
|
|
||||||
"github.com/mochi-mqtt/server/v2/packets"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestTCPNoDelay 测试 TCP_NODELAY 是否正确设置
|
|
||||||
func TestTCPNoDelay(t *testing.T) {
|
|
||||||
// 创建一个模拟的 TCP 连接
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
|
||||||
|
|
||||||
// 模拟客户端连接
|
|
||||||
connChan := make(chan net.Conn, 1)
|
|
||||||
go func() {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to accept connection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
connChan <- conn
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 客户端连接
|
|
||||||
clientConn, err := net.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to dial: %v", err)
|
|
||||||
}
|
|
||||||
defer clientConn.Close()
|
|
||||||
|
|
||||||
// 等待服务器端接受连接
|
|
||||||
serverConn := <-connChan
|
|
||||||
defer serverConn.Close()
|
|
||||||
|
|
||||||
// 创建 MQTT Client 包装
|
|
||||||
cl := &mqtt.Client{
|
|
||||||
Net: mqtt.ClientConnection{
|
|
||||||
Conn: serverConn,
|
|
||||||
Remote: serverConn.RemoteAddr().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 hook 并调用 OnConnect
|
|
||||||
hook := &meshtasticFilterHook{}
|
|
||||||
pk := packets.Packet{}
|
|
||||||
|
|
||||||
err = hook.OnConnect(cl, pk)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("OnConnect failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 TCP_NODELAY 是否设置
|
|
||||||
if tcpConn, ok := serverConn.(*net.TCPConn); ok {
|
|
||||||
// 这里我们无法直接读取 TCP_NODELAY 的值,但可以验证没有错误
|
|
||||||
// 实际上,我们可以通过设置后再次设置来验证
|
|
||||||
err := tcpConn.SetNoDelay(false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to set NoDelay to false: %v", err)
|
|
||||||
}
|
|
||||||
err = tcpConn.SetNoDelay(true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to set NoDelay to true: %v", err)
|
|
||||||
}
|
|
||||||
t.Log("TCP_NODELAY successfully set")
|
|
||||||
} else {
|
|
||||||
t.Fatal("Connection is not a TCP connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestQoS0MessageLatency 测试 QoS0 消息的响应延迟
|
|
||||||
func TestQoS0MessageLatency(t *testing.T) {
|
|
||||||
// 创建一个简单的 TCP echo 服务器来模拟 MQTT
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
go func() {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 设置 TCP_NODELAY
|
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
||||||
tcpConn.SetNoDelay(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
for {
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 立即写回(模拟 ACK)
|
|
||||||
_, err = conn.Write(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 客户端连接
|
|
||||||
conn, err := net.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to dial: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 测试小数据包的延迟
|
|
||||||
testData := []byte("test")
|
|
||||||
samples := 10
|
|
||||||
var totalLatency time.Duration
|
|
||||||
|
|
||||||
for i := 0; i < samples; i++ {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
_, err := conn.Write(testData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Write failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, len(testData))
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
latency := time.Since(start)
|
|
||||||
totalLatency += latency
|
|
||||||
t.Logf("Round trip %d: %v", i+1, latency)
|
|
||||||
}
|
|
||||||
|
|
||||||
avgLatency := totalLatency / time.Duration(samples)
|
|
||||||
t.Logf("Average latency: %v", avgLatency)
|
|
||||||
|
|
||||||
// 平均延迟应该小于 10ms(如果没有 Nagle 算法延迟)
|
|
||||||
if avgLatency > 10*time.Millisecond {
|
|
||||||
t.Logf("Warning: Average latency %v is higher than expected, may indicate Nagle's algorithm is active", avgLatency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user