Rewind put an object's old form at the end of its list, so a history entry's diff showed the changed object moving (turning hairpin off for one forward also moved its forward and DNAT lines). It now sorts after stepping back. go-udiff v0.4.1 numbers the new side of hunks after one with a moved line too early, so the chain after @@ could be the one before; hunkStarts numbers them again. History diffs name their sides "before N" and "after N". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
5.3 KiB
Go
140 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
udiff "github.com/aymanbagabas/go-udiff"
|
|
|
|
"housecarl/internal/api"
|
|
"housecarl/internal/model"
|
|
)
|
|
|
|
// TestBlockHeaders: each hunk says which chain it is in, even when the
|
|
// chain's own line is far above it.
|
|
func TestBlockHeaders(t *testing.T) {
|
|
var b strings.Builder
|
|
b.WriteString("table inet housecarl {\n\tset lan_v4 { type ipv4_addr; }\n\tchain input {\n")
|
|
for i := 0; i < 20; i++ {
|
|
b.WriteString("\t\tcounter comment \"in\"\n")
|
|
}
|
|
b.WriteString("\t}\n\tchain forward {\n")
|
|
for i := 0; i < 20; i++ {
|
|
b.WriteString("\t\tcounter comment \"fw\"\n")
|
|
}
|
|
b.WriteString("\t}\n}\n")
|
|
now := b.String()
|
|
then := strings.Replace(now, "\t\tcounter comment \"in\"\n\t}", "\t\tcounter comment \"in\"\n\t\taccept comment \"new in\"\n\t}", 1)
|
|
then = strings.Replace(then, "\t\tcounter comment \"fw\"\n\t}", "\t\taccept comment \"new fw\"\n\t}", 1)
|
|
|
|
diff := blockHeaders(udiff.Unified("a", "b", now, then), now, then)
|
|
var heads []string
|
|
for _, l := range strings.Split(diff, "\n") {
|
|
if strings.HasPrefix(l, "@@") {
|
|
heads = append(heads, l)
|
|
}
|
|
}
|
|
if len(heads) != 2 || !strings.HasSuffix(heads[0], "@@ chain input") || !strings.HasSuffix(heads[1], "@@ chain forward") {
|
|
t.Fatalf("hunk headers %q, want chain input then chain forward\n%s", heads, diff)
|
|
}
|
|
if blockHeaders("", now, then) != "" {
|
|
t.Fatal("no diff should stay empty")
|
|
}
|
|
}
|
|
|
|
// TestHistoryDiff: an entry's diff is its own change, rebuilt from the
|
|
// current configuration however many entries came after it.
|
|
func TestHistoryDiff(t *testing.T) {
|
|
e := newEnv(t)
|
|
var mr api.MutationResponse
|
|
e.call(e.token, "POST", "/zones", model.Zone{Name: "lan", Interfaces: []string{"eth1"}}, nil)
|
|
e.call(e.token, "POST", "/rules", model.Rule{Chain: "input", Match: model.Match{Services: []string{"ssh"}}, Comment: "first"}, &mr)
|
|
first := mr.HistoryID
|
|
e.call(e.token, "POST", "/rules", model.Rule{Chain: "forward", Match: model.Match{Services: []string{"http"}}, Comment: "second"}, nil)
|
|
e.call(e.token, "PATCH", "/settings", map[string]any{"builtins": map[string]any{"drop_invalid": true}}, nil)
|
|
|
|
var out struct{ Diff string }
|
|
if code := e.call(e.token, "GET", "/history/"+itoa(first)+"/diff", nil, &out); code != 200 {
|
|
t.Fatalf("diff: %d", code)
|
|
}
|
|
var added []string
|
|
for _, l := range strings.Split(out.Diff, "\n") {
|
|
if strings.HasPrefix(l, "+") && !strings.HasPrefix(l, "+++") {
|
|
added = append(added, l)
|
|
}
|
|
}
|
|
if len(added) == 0 || !strings.Contains(strings.Join(added, "\n"), ` first"`) || strings.Contains(out.Diff, ` second"`) || !strings.Contains(out.Diff, "@@ chain input") {
|
|
t.Errorf("entry %d's diff:\n%s", first, out.Diff)
|
|
}
|
|
if code := e.call(e.token, "GET", "/history/9999/diff", nil, nil); code != 404 {
|
|
t.Errorf("unknown entry: %d", code)
|
|
}
|
|
}
|
|
|
|
// TestHunkStarts: after a hunk with unchanged lines between its edits (a
|
|
// moved line), the later hunks' new sides start where they do.
|
|
func TestHunkStarts(t *testing.T) {
|
|
var a, b strings.Builder
|
|
for i := 1; i <= 20; i++ {
|
|
fmt.Fprintf(&a, "%d\n", i)
|
|
switch i {
|
|
case 4:
|
|
case 6:
|
|
b.WriteString("6\n4\n")
|
|
case 14:
|
|
default:
|
|
fmt.Fprintf(&b, "%d\n", i)
|
|
}
|
|
}
|
|
diff := hunkStarts(udiff.Unified("a", "b", a.String(), b.String()))
|
|
var heads []string
|
|
for _, l := range strings.Split(diff, "\n") {
|
|
if strings.HasPrefix(l, "@@") {
|
|
heads = append(heads, l)
|
|
}
|
|
}
|
|
// diff -u: the move nets nothing, the deleted 14 one line.
|
|
if want := []string{"@@ -1,9 +1,9 @@", "@@ -11,7 +11,6 @@"}; strings.Join(heads, "|") != strings.Join(want, "|") {
|
|
t.Fatalf("hunk headers %q, want %q\n%s", heads, want, diff)
|
|
}
|
|
}
|
|
|
|
// TestHistoryDiffUpdate: an update's diff is only what it changed; the
|
|
// object stepped back to its old form stays where it was.
|
|
func TestHistoryDiffUpdate(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.call(e.token, "POST", "/zones", model.Zone{Name: "lan", Interfaces: []string{"eth1"}}, nil)
|
|
e.call(e.token, "POST", "/zones", model.Zone{Name: "wan", Interfaces: []string{"wan"}, External: true}, nil)
|
|
var ids []int64
|
|
for i, port := range []string{"8096", "443", "50300", "7777", "25"} {
|
|
var mr api.MutationResponse
|
|
e.call(e.token, "POST", "/forwards", map[string]any{"name": "f" + port, "ports": port, "to": fmt.Sprintf("10.0.10.%d", 30+i),
|
|
"public_addresses": []string{"203.0.113.1"}, "hairpin": true}, &mr)
|
|
var f model.PortForward
|
|
if err := json.Unmarshal(mr.Object, &f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids = append(ids, f.ID)
|
|
}
|
|
var mr api.MutationResponse
|
|
if code := e.call(e.token, "PATCH", "/forwards/"+itoa(ids[2]), map[string]any{"hairpin": false}, &mr); code != 200 {
|
|
t.Fatalf("update: %d", code)
|
|
}
|
|
var out struct{ Diff string }
|
|
if code := e.call(e.token, "GET", "/history/"+itoa(mr.HistoryID)+"/diff", nil, &out); code != 200 {
|
|
t.Fatalf("diff: %d", code)
|
|
}
|
|
var changed []string
|
|
for _, l := range strings.Split(out.Diff, "\n") {
|
|
if (strings.HasPrefix(l, "+") || strings.HasPrefix(l, "-")) && !strings.HasPrefix(l, "+++") && !strings.HasPrefix(l, "---") {
|
|
changed = append(changed, l)
|
|
}
|
|
}
|
|
if len(changed) != 1 || !strings.HasPrefix(changed[0], "-") || !strings.Contains(changed[0], "-hairpin") ||
|
|
!strings.Contains(out.Diff, "(before "+itoa(mr.HistoryID)+")") || !strings.Contains(out.Diff, "@@ chain srcnat") {
|
|
t.Errorf("entry %d's diff, want only the hairpin line gone, in chain srcnat:\n%s", mr.HistoryID, out.Diff)
|
|
}
|
|
}
|