mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
943818f7d4
将 OpenFlare 与平台业务的数据访问从 model 与 apps 直连迁入 repository, model 仅保留实体与无 IO 规则;补充 code-check 架构守卫与开发规范。
158 lines
6.4 KiB
Go
158 lines
6.4 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package config_version
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
|
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBuildSnapshotRejectsOversizedAggregateWAFIPGroups(t *testing.T) {
|
|
cleanup := setupConfigVersionTestDB(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
// Each group remains below the existing 2 MiB per-subscription ceiling,
|
|
// while the complete Agent runtime document exceeds the aggregate limit.
|
|
ipList, err := json.Marshal(strings.Fields(strings.Repeat("192.0.2.1 ", 165000)))
|
|
require.NoError(t, err)
|
|
require.Less(t, len(ipList), 2<<20)
|
|
|
|
groupIDs := make([]uint, 0, 12)
|
|
for index := 0; index < 12; index++ {
|
|
group := &model.OpenFlareWAFIPGroup{
|
|
Name: "aggregate-" + strings.Repeat("x", index),
|
|
Type: "manual",
|
|
Enabled: true,
|
|
IPList: string(ipList),
|
|
}
|
|
require.NoError(t, db.DB(ctx).Create(group).Error)
|
|
groupIDs = append(groupIDs, group.ID)
|
|
}
|
|
createSnapshotRule(t, ctx, "oversized-aggregate", snapshotIPMatchGraphForGroups(groupIDs))
|
|
|
|
_, err = buildSnapshotWAFDocument(ctx, nil)
|
|
require.ErrorContains(t, err, "WAF IP 组快照大小")
|
|
require.ErrorContains(t, err, "超过上限")
|
|
}
|
|
|
|
func TestWAFGraphSnapshotPreservesOrderAndGraphReferences(t *testing.T) {
|
|
cleanup := setupConfigVersionTestDB(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
route := &model.ProxyRoute{SiteName: "ordered.example.com", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true}
|
|
require.NoError(t, repository.CreateProxyRouteRecord(ctx, route))
|
|
createSnapshotZoneDomains(t, ctx, route, route.SiteName)
|
|
|
|
referenced := &model.OpenFlareWAFIPGroup{Name: "referenced", Type: "manual", Enabled: true, IPList: `["192.0.2.1"]`}
|
|
unused := &model.OpenFlareWAFIPGroup{Name: "unused", Type: "manual", Enabled: true, IPList: `["198.51.100.1"]`}
|
|
require.NoError(t, db.DB(ctx).Create(referenced).Error)
|
|
require.NoError(t, db.DB(ctx).Create(unused).Error)
|
|
|
|
customA := createSnapshotRule(t, ctx, "custom-a", waf.DefaultRuleGraph())
|
|
customB := createSnapshotRule(t, ctx, "custom-b", snapshotIPMatchGraph(referenced.ID))
|
|
require.NoError(t, repository.ReplaceOpenFlareWAFSiteRuleGroupBindings(ctx, route.ID, []uint{customB.ID, customA.ID}))
|
|
|
|
snapshot, err := buildSnapshotWAFDocument(ctx, []*model.ProxyRoute{route})
|
|
require.NoError(t, err)
|
|
require.Len(t, snapshot.Bindings, 1)
|
|
assert.Equal(t, []uint{customB.ID, customA.ID}, snapshot.Bindings[0].RuleGroupIDs)
|
|
require.Len(t, snapshot.IPGroups, 1)
|
|
assert.Equal(t, referenced.ID, snapshot.IPGroups[0].ID)
|
|
|
|
var customBSnapshot *snapshotWAFRuleGroup
|
|
for index := range snapshot.RuleGroups {
|
|
if snapshot.RuleGroups[index].ID == customB.ID {
|
|
customBSnapshot = &snapshot.RuleGroups[index]
|
|
}
|
|
}
|
|
require.NotNil(t, customBSnapshot)
|
|
assert.Equal(t, "start", customBSnapshot.Graph.Entry)
|
|
assert.Equal(t, waf.RuleNodeIPMatch, customBSnapshot.Graph.Nodes["match"].Type)
|
|
raw, err := json.Marshal(customBSnapshot)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(raw), "position")
|
|
assert.NotContains(t, string(raw), "ip_whitelist")
|
|
}
|
|
|
|
func TestWAFGraphSnapshotEncodesEmptyBindingsAsArrays(t *testing.T) {
|
|
cleanup := setupConfigVersionTestDB(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
route := &model.ProxyRoute{SiteName: "empty-binding.example.com", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true}
|
|
require.NoError(t, repository.CreateProxyRouteRecord(ctx, route))
|
|
createSnapshotZoneDomains(t, ctx, route, route.SiteName)
|
|
|
|
snapshot, err := buildSnapshotWAFDocument(ctx, []*model.ProxyRoute{route})
|
|
require.NoError(t, err)
|
|
require.Len(t, snapshot.Bindings, 1)
|
|
require.NotNil(t, snapshot.Bindings[0].RuleGroupIDs)
|
|
|
|
raw, err := json.Marshal(snapshot)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(raw), `"rule_group_ids":[]`)
|
|
assert.NotContains(t, string(raw), `"rule_group_ids":null`)
|
|
}
|
|
|
|
func TestBuildSnapshotRejectsInvalidWAFGraph(t *testing.T) {
|
|
cleanup := setupConfigVersionTestDB(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
invalid := &model.OpenFlareWAFRuleGroup{Name: "invalid", Enabled: true, Graph: `{"schema_version":1,"nodes":[],"edges":[]}`, Revision: 1}
|
|
require.NoError(t, db.DB(ctx).Create(invalid).Error)
|
|
_, err := buildSnapshotWAFDocument(ctx, nil)
|
|
require.ErrorContains(t, err, "invalid")
|
|
}
|
|
|
|
func createSnapshotRule(t *testing.T, ctx context.Context, name string, graph waf.RuleGraph) *model.OpenFlareWAFRuleGroup {
|
|
t.Helper()
|
|
raw, err := json.Marshal(graph)
|
|
require.NoError(t, err)
|
|
rule := &model.OpenFlareWAFRuleGroup{Name: name, Enabled: true, Graph: string(raw), Revision: 1}
|
|
require.NoError(t, db.DB(ctx).Create(rule).Error)
|
|
return rule
|
|
}
|
|
|
|
func snapshotIPMatchGraph(ipGroupID uint) waf.RuleGraph {
|
|
return snapshotIPMatchGraphForGroups([]uint{ipGroupID})
|
|
}
|
|
|
|
func snapshotIPMatchGraphForGroups(ipGroupIDs []uint) waf.RuleGraph {
|
|
config, _ := json.Marshal(waf.IPMatchConfig{IPGroupIDs: ipGroupIDs})
|
|
return waf.RuleGraph{SchemaVersion: waf.RuleGraphSchemaVersion, Nodes: []waf.RuleNode{
|
|
{ID: "start", Type: waf.RuleNodeStart, Position: waf.RulePosition{X: 1, Y: 2}, Config: json.RawMessage(`{}`)},
|
|
{ID: "match", Type: waf.RuleNodeIPMatch, Position: waf.RulePosition{X: 3, Y: 4}, Config: config},
|
|
{ID: "allow", Type: waf.RuleNodeAllow, Position: waf.RulePosition{X: 5, Y: 6}, Config: json.RawMessage(`{}`)},
|
|
}, Edges: []waf.RuleEdge{
|
|
{ID: "e1", Source: "start", SourceHandle: "next", Target: "match"},
|
|
{ID: "e2", Source: "match", SourceHandle: "true", Target: "allow"},
|
|
{ID: "e3", Source: "match", SourceHandle: "false", Target: "allow"},
|
|
}}
|
|
}
|
|
|
|
func snapshotPoWGraph() waf.RuleGraph {
|
|
config, _ := json.Marshal(waf.PoWNodeConfig{Algorithm: "fast", Difficulty: 4, SessionTTL: 600, ChallengeTTL: 300})
|
|
return waf.RuleGraph{SchemaVersion: waf.RuleGraphSchemaVersion, Nodes: []waf.RuleNode{
|
|
{ID: "start", Type: waf.RuleNodeStart, Config: json.RawMessage(`{}`)},
|
|
{ID: "pow", Type: waf.RuleNodePoW, Config: config},
|
|
{ID: "allow", Type: waf.RuleNodeAllow, Config: json.RawMessage(`{}`)},
|
|
}, Edges: []waf.RuleEdge{
|
|
{ID: "e1", Source: "start", SourceHandle: "next", Target: "pow"},
|
|
{ID: "e2", Source: "pow", SourceHandle: "next", Target: "allow"},
|
|
}}
|
|
}
|