Skip to content

Commit e0fdee4

Browse files
h0tak88rclaude
andcommitted
fix(test): stop .gitignore from hiding *_test.go; add target-sanitizer tests
The `*test*` pattern in .gitignore matched every Go `*_test.go` file, so the test suite was silently untracked and CI's `go test ./...` never ran it (only tracked files are checked out in CI). Add a `!*_test.go` negation so test files are version-controlled again, and commit the regression test for utils.SanitizeTargetSegment (the path-traversal guard added in the previous commit). Note: ~32 other pre-existing *_test.go files are now un-ignored and show as untracked — they should be added in a follow-up so CI actually exercises them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 231d88d commit e0fdee4

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ logs/
6060
# Test files and directories
6161
test-*
6262
*test*
63+
!*_test.go
6364
!tests/
6465
!tests/ui/
6566
!tests/ui/*.test.js

internal/utils/utils_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package utils
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestSanitizeTargetSegmentPreservesLegitTargets(t *testing.T) {
10+
cases := map[string]string{
11+
"example.com": "example.com",
12+
"https://example.com": "example.com",
13+
"http://api.example.io": "api.example.io",
14+
"sub.example.com/": "sub.example.com",
15+
"example.com:8080": "example.com-8080",
16+
"my-bucket_name": "my-bucket_name",
17+
}
18+
for in, want := range cases {
19+
if got := SanitizeTargetSegment(in); got != want {
20+
t.Errorf("SanitizeTargetSegment(%q) = %q, want %q", in, got, want)
21+
}
22+
}
23+
}
24+
25+
// TestSanitizeTargetSegmentBlocksTraversal is the security regression guard:
26+
// no sanitized segment may contain a path separator or "..", so it can never
27+
// escape its parent dir via filepath.Join.
28+
func TestSanitizeTargetSegmentBlocksTraversal(t *testing.T) {
29+
evil := []string{
30+
"../../../../etc/passwd",
31+
"..\\..\\windows\\system32",
32+
"../../tmp/x",
33+
"a/b/c",
34+
"....//....//x",
35+
"foo/../../bar",
36+
}
37+
base := "/app/new-results"
38+
for _, in := range evil {
39+
got := SanitizeTargetSegment(in)
40+
if strings.ContainsAny(got, `/\`) {
41+
t.Errorf("SanitizeTargetSegment(%q) = %q still contains a path separator", in, got)
42+
}
43+
if strings.Contains(got, "..") {
44+
t.Errorf("SanitizeTargetSegment(%q) = %q still contains '..'", in, got)
45+
}
46+
joined := filepath.Join(base, got)
47+
if !strings.HasPrefix(joined, base+string(filepath.Separator)) {
48+
t.Errorf("filepath.Join(%q, %q) = %q escaped the base dir", base, got, joined)
49+
}
50+
}
51+
}
52+
53+
func TestSanitizeTargetSegmentEmpty(t *testing.T) {
54+
if got := SanitizeTargetSegment(""); got != "unknown" {
55+
t.Errorf("SanitizeTargetSegment(\"\") = %q, want %q", got, "unknown")
56+
}
57+
if got := SanitizeTargetSegment("..."); got != "unknown" {
58+
t.Errorf("SanitizeTargetSegment(\"...\") = %q, want %q", got, "unknown")
59+
}
60+
}
61+
62+
func TestUniqueStringsEmpty(t *testing.T) {
63+
got := UniqueStrings(nil)
64+
if got != nil && len(got) != 0 {
65+
t.Errorf("UniqueStrings(nil) = %v, want nil or empty", got)
66+
}
67+
}
68+
69+
func TestUniqueStringsNoDupes(t *testing.T) {
70+
in := []string{"a", "b", "c"}
71+
got := UniqueStrings(in)
72+
if len(got) != 3 {
73+
t.Errorf("UniqueStrings() len = %d, want 3", len(got))
74+
}
75+
for i, v := range []string{"a", "b", "c"} {
76+
if got[i] != v {
77+
t.Errorf("UniqueStrings()[%d] = %q, want %q", i, got[i], v)
78+
}
79+
}
80+
}
81+
82+
func TestUniqueStringsDeduplicates(t *testing.T) {
83+
in := []string{"a", "b", "a", "c", "b"}
84+
got := UniqueStrings(in)
85+
want := []string{"a", "b", "c"}
86+
if len(got) != len(want) {
87+
t.Fatalf("UniqueStrings() len = %d, want %d: %v", len(got), len(want), got)
88+
}
89+
for i, v := range want {
90+
if got[i] != v {
91+
t.Errorf("UniqueStrings()[%d] = %q, want %q", i, got[i], v)
92+
}
93+
}
94+
}
95+
96+
func TestUniqueStringsAllSame(t *testing.T) {
97+
in := []string{"x", "x", "x", "x"}
98+
got := UniqueStrings(in)
99+
if len(got) != 1 || got[0] != "x" {
100+
t.Errorf("UniqueStrings() = %v, want [x]", got)
101+
}
102+
}
103+
104+
func TestUniqueStringsWhitespaceFiltering(t *testing.T) {
105+
in := []string{" a ", "a", " b "}
106+
got := UniqueStrings(in)
107+
if len(got) != 2 {
108+
t.Errorf("UniqueStrings() len = %d, want 2: %v", len(got), got)
109+
}
110+
}
111+
112+
func TestUniqueStringsEmptyStringFiltered(t *testing.T) {
113+
in := []string{"", "a", ""}
114+
got := UniqueStrings(in)
115+
if len(got) != 1 || got[0] != "a" {
116+
t.Errorf("UniqueStrings() = %v, want [a]", got)
117+
}
118+
}
119+
120+
func TestUniqueStringsPreservesOrder(t *testing.T) {
121+
in := []string{"z", "a", "m", "a", "z", "b"}
122+
got := UniqueStrings(in)
123+
want := []string{"z", "a", "m", "b"}
124+
if len(got) != len(want) {
125+
t.Fatalf("UniqueStrings() len = %d, want %d: %v", len(got), len(want), got)
126+
}
127+
for i, v := range want {
128+
if got[i] != v {
129+
t.Errorf("UniqueStrings()[%d] = %q, want %q", i, got[i], v)
130+
}
131+
}
132+
}
133+
134+
func TestURLSlugSimple(t *testing.T) {
135+
got := URLSlug("https://example.com/path/to/page")
136+
if got != "https:__example.com_path_to_page" {
137+
t.Errorf("URLSlug() = %q, want %q", got, "https:__example.com_path_to_page")
138+
}
139+
}
140+
141+
func TestURLSlugNoSlashes(t *testing.T) {
142+
got := URLSlug("example.com")
143+
if got != "example.com" {
144+
t.Errorf("URLSlug() = %q, want %q", got, "example.com")
145+
}
146+
}
147+
148+
func TestURLSlugEmpty(t *testing.T) {
149+
got := URLSlug("")
150+
if got != "" {
151+
t.Errorf("URLSlug() = %q, want %q", got, "")
152+
}
153+
}
154+
155+
func TestURLSlugMultipleSlashes(t *testing.T) {
156+
got := URLSlug("a//b")
157+
if got != "a__b" {
158+
t.Errorf("URLSlug() = %q, want %q", got, "a__b")
159+
}
160+
}

0 commit comments

Comments
 (0)