check / check (push) Successful in 3m32s
Each client, one IPv4 address or one IPv6 /64, has its requests counted in two buckets per window, the earlier weighted by how much of it the window still covers, in a table of at most 20,000 clients that drops the least recently seen. A request over SWWAF_RATE_LIMIT_PER_MINUTE, _HOUR or _DAY (1000, 10000, 50000, or off) gets 429 before anything reaches the app, and refused requests count. The log line gains limit_hit and the action rate_limited. The rate limits run before the announced-size check, so a request refused with 413 is counted too. Deviation from SPEC.md, per the issue: the 20,000 bound and the /64 are fixed, not settings. Judgement call: golang-lru/v2 holds the table; httprate is not used, as it reads the wall clock and does not count refused requests. Deviation: go.mod and go.sum were written by hand from the Go checksum database, as no make target runs go mod tidy. Model: opus-5-5
91 lines
2.2 KiB
Go
91 lines
2.2 KiB
Go
package requestlog_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
err := requestlog.Write(&out, &requestlog.Line{
|
|
Time: requestlog.FormatTime(time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)),
|
|
ClientIP: "203.0.113.9",
|
|
Status: 200,
|
|
Action: requestlog.ActionForward,
|
|
DurationTotal: requestlog.Milliseconds(1500 * time.Microsecond),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
text := out.String()
|
|
if strings.Count(text, "\n") != 1 || !strings.HasSuffix(text, "\n") {
|
|
t.Fatalf("wrote %q, want one line", text)
|
|
}
|
|
|
|
var fields map[string]any
|
|
|
|
err = json.Unmarshal(out.Bytes(), &fields)
|
|
if err != nil {
|
|
t.Fatalf("decode %q: %v", text, err)
|
|
}
|
|
|
|
want := map[string]any{
|
|
"type": "request", "time": "2026-10-03T12:00:00.000Z",
|
|
"client_ip": "203.0.113.9", "status": 200.0, "action": "forward",
|
|
"duration_total": 1.5,
|
|
}
|
|
for name, value := range want {
|
|
if fields[name] != value {
|
|
t.Errorf("%s is %v, want %v", name, fields[name], value)
|
|
}
|
|
}
|
|
|
|
unset := []string{
|
|
"upstream_status", "limit_hit", "aborted", "duration_upstream_total",
|
|
}
|
|
for _, name := range unset {
|
|
_, present := fields[name]
|
|
if present {
|
|
t.Errorf("%s is there with no value to give", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProcessLinesAreMarkedProcess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
|
|
requestlog.NewProcessLogger(&out).Info("starting", "version", "v1")
|
|
|
|
var fields map[string]any
|
|
|
|
err := json.Unmarshal(out.Bytes(), &fields)
|
|
if err != nil {
|
|
t.Fatalf("decode %q: %v", out.String(), err)
|
|
}
|
|
|
|
if fields["type"] != "process" || fields["msg"] != "starting" ||
|
|
fields["level"] != "INFO" || fields["version"] != "v1" {
|
|
t.Errorf("process line %v", fields)
|
|
}
|
|
|
|
timeText, _ := fields["time"].(string)
|
|
|
|
logged, err := time.Parse(time.RFC3339, timeText)
|
|
if err != nil || !strings.HasSuffix(timeText, "Z") ||
|
|
len(timeText) != len("2006-01-02T15:04:05.000Z") ||
|
|
time.Since(logged) > time.Minute {
|
|
t.Errorf("process line time %q, want now in UTC with milliseconds", timeText)
|
|
}
|
|
}
|