Files
OpenFlare/internal/repository/analytics/node_access_log_wildcard_test.go
T
Deqiying 9a51a68206 feat(domain): 支持通配符域名及关联功能适配 (#63)
* feat(domain): 支持通配符域名及关联功能适配

* fix(domain): preserve legacy Unicode imports and address review findings

* fix(domain): canonicalize legacy IDNs and exclude empty host labels

* fix(domain): normalize equivalent Unicode IDN spellings

---------

Co-authored-by: Ryan <63696351+Rain-kl@users.noreply.github.com>
Co-authored-by: ryan <ryan@arctel.net>
2026-11-09 10:31:03 +08:00

133 lines
4.8 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package analytics
import (
"context"
"strings"
"testing"
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/google/go-cmp/cmp"
)
func TestNodeAccessLogHostsWildcardSQL(t *testing.T) {
since := time.Date(2026, time.June, 1, 0, 0, 0, 0, time.UTC)
until := since.Add(time.Hour)
tests := []struct {
name string
filter NodeAccessLogFilter
clause string
args []any
}{
{
name: "exact_hosts_remain_exact_and_normalized",
filter: NodeAccessLogFilter{Hosts: []string{" API.Example.com ", "api.example.com", "", "www.example.com"}},
clause: "lowerUTF8(trim(host)) IN (?, ?)",
args: []any{"api.example.com", "www.example.com"},
},
{
name: "wildcard_requires_nonempty_prefix",
filter: NodeAccessLogFilter{Hosts: []string{" *.Example.com ", "*.example.com"}},
clause: "(lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%')",
args: []any{"_%.example.com"},
},
{
name: "nested_wildcard",
filter: NodeAccessLogFilter{Hosts: []string{"*.s3.example.com"}},
clause: "(lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%')",
args: []any{"_%.s3.example.com"},
},
{
name: "overlapping_scopes_are_one_group_with_other_filters",
filter: NodeAccessLogFilter{
NodeID: " n1 ", Host: "ignored", Hosts: []string{"api.example.com", "*.example.com", "*.s3.example.com"},
StatusCode: 200, Since: since, Until: until,
},
clause: "node_id = ? AND ((lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%') OR (lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%') OR lowerUTF8(trim(host)) IN (?)) AND status_code = ? AND logged_at >= ? AND logged_at < ?",
args: []any{"n1", "_%.example.com", "_%.s3.example.com", "api.example.com", 200, since, until},
},
{
name: "wildcard_suffix_like_metacharacters_are_literal",
filter: NodeAccessLogFilter{Hosts: []string{`*.a%_\'?.example.com`}},
clause: "(lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%')",
args: []any{`_%.a\%\_\\'?.example.com`},
},
{
name: "single_host_keeps_prefix_search",
filter: NodeAccessLogFilter{Host: " *.ex%_ ", Hosts: []string{" "}},
clause: "host LIKE ?",
args: []any{`*.ex\%\_%`},
},
{
name: "non_leading_wildcards_are_exact",
filter: NodeAccessLogFilter{Hosts: []string{"api.*.example.com", "*", "*."}},
clause: "lowerUTF8(trim(host)) IN (?, ?, ?)",
args: []any{"api.*.example.com", "*", "*."},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clause, args := buildNodeAccessLogFilterClause(tt.filter)
if clause != tt.clause {
t.Errorf("buildNodeAccessLogFilterClause(%+v) SQL = %q, want %q", tt.filter, clause, tt.clause)
}
if diff := cmp.Diff(tt.args, args); diff != "" {
t.Errorf("buildNodeAccessLogFilterClause(%+v) args mismatch (-want +got):\n%s", tt.filter, diff)
}
})
}
}
// wildcardQueryConn captures both single-row and multi-row query entry points.
type wildcardQueryConn struct{ mockConn }
func (m *wildcardQueryConn) QueryRow(_ context.Context, query string, args ...any) driver.Row {
m.queries = append(m.queries, query)
m.queryArgs = append(m.queryArgs, args)
return wildcardCountRow{}
}
type wildcardCountRow struct{ driver.Row }
func (wildcardCountRow) Scan(dest ...any) error {
for _, value := range dest {
*value.(*uint64) = 0
}
return nil
}
// TestWildcardQueryEntryPoints protects host filtering in count and bucket queries.
func TestWildcardQueryEntryPoints(t *testing.T) {
conn := &wildcardQueryConn{}
previous := db.ChConn
db.SetChConnForTest(conn)
t.Cleanup(func() { db.SetChConnForTest(previous) })
ctx := context.Background()
filter := NodeAccessLogFilter{Hosts: []string{"*.Example.com", "api.example.com"}}
if _, _, _, err := CountNodeAccessLogs(ctx, filter); err != nil {
t.Fatal(err)
}
if _, err := BucketAggregatesNodeAccessLogs(ctx, filter, 300); err != nil {
t.Fatal(err)
}
if _, err := CountBucketAggregatesNodeAccessLogs(ctx, filter, 300); err != nil {
t.Fatal(err)
}
if len(conn.queries) != 3 {
t.Fatalf("count/bucket query calls = %d, want 3", len(conn.queries))
}
for i, query := range conn.queries {
if !strings.Contains(query, "WHERE ((lowerUTF8(trim(host)) LIKE ? AND trim(host) NOT LIKE '%..%' AND trim(host) NOT LIKE '.%') OR lowerUTF8(trim(host)) IN (?))") {
t.Errorf("count/bucket SQL = %q, want grouped wildcard and exact host predicates", query)
}
if diff := cmp.Diff([]any{"_%.example.com", "api.example.com"}, conn.queryArgs[i]); diff != "" {
t.Errorf("count/bucket args mismatch (-want +got):\n%s", diff)
}
}
}