Skip to content

Commit 3eb1023

Browse files
committed
test: add PathCache LRU and server security-defaults coverage
- Add TestPathCache_BoundedLRU: Len() never exceeds maxEntries after overflow - Add TestPathCache_LookupPromotesEntry: LRU promotion keeps touched keys - Add TestPathCache_FlushClearsAll: Purge empties cache completely - Add TestPathCache_DefaultSizeOnZero: fallback to DefaultPathCacheSize - Add TestNew_HTTPOnly_SecurityDefaults: Name, MaxRequestBodySize, MaxConnsPerIP - Add TestNew_TLS_SecurityDefaults: same checks on both HTTP and HTTPS servers - Add TestNew_MaxConnsPerIP_Zero: disabled state passes through correctly
1 parent a19a40b commit 3eb1023

2 files changed

Lines changed: 210 additions & 0 deletions

File tree

internal/security/security_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package security_test
22

33
import (
44
"errors"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"strings"
@@ -383,6 +384,112 @@ func TestMiddleware_CORS_NoCORSConfigured(t *testing.T) {
383384
}
384385
}
385386

387+
// ---------------------------------------------------------------------------
388+
// PathCache bounded LRU behaviour (SEC-001)
389+
// ---------------------------------------------------------------------------
390+
391+
func TestPathCache_BoundedLRU(t *testing.T) {
392+
const maxEntries = 8
393+
394+
pc := security.NewPathCache(maxEntries)
395+
396+
// Fill the cache to capacity.
397+
for i := 0; i < maxEntries; i++ {
398+
key := fmt.Sprintf("/page/%d", i)
399+
pc.Store(key, "/safe/"+key)
400+
}
401+
if pc.Len() != maxEntries {
402+
t.Fatalf("Len() = %d after filling, want %d", pc.Len(), maxEntries)
403+
}
404+
405+
// Insert more entries — Len() must never exceed maxEntries.
406+
overflow := maxEntries * 3
407+
for i := maxEntries; i < maxEntries+overflow; i++ {
408+
key := fmt.Sprintf("/page/%d", i)
409+
pc.Store(key, "/safe/"+key)
410+
if pc.Len() > maxEntries {
411+
t.Fatalf("Len() = %d after inserting key %q, exceeds max %d",
412+
pc.Len(), key, maxEntries)
413+
}
414+
}
415+
416+
// Recently-used keys should still be retrievable.
417+
lastKey := fmt.Sprintf("/page/%d", maxEntries+overflow-1)
418+
if val, ok := pc.Lookup(lastKey); !ok {
419+
t.Errorf("recently-inserted key %q missing from cache", lastKey)
420+
} else if val != "/safe/"+lastKey {
421+
t.Errorf("Lookup(%q) = %q, want %q", lastKey, val, "/safe/"+lastKey)
422+
}
423+
424+
// Oldest keys (from the first batch) should have been evicted.
425+
oldKey := "/page/0"
426+
if _, ok := pc.Lookup(oldKey); ok {
427+
t.Errorf("oldest key %q should have been evicted but was found", oldKey)
428+
}
429+
}
430+
431+
func TestPathCache_LookupPromotesEntry(t *testing.T) {
432+
const maxEntries = 4
433+
434+
pc := security.NewPathCache(maxEntries)
435+
436+
// Fill to capacity: keys /a, /b, /c, /d (in insertion order).
437+
for _, k := range []string{"/a", "/b", "/c", "/d"} {
438+
pc.Store(k, "/safe"+k)
439+
}
440+
441+
// Touch /a so it becomes most-recently-used.
442+
if _, ok := pc.Lookup("/a"); !ok {
443+
t.Fatal("/a should be in cache")
444+
}
445+
446+
// Insert two new keys to force two evictions.
447+
pc.Store("/e", "/safe/e")
448+
pc.Store("/f", "/safe/f")
449+
450+
// /a should survive (it was promoted by the Lookup).
451+
if _, ok := pc.Lookup("/a"); !ok {
452+
t.Error("/a should still be in cache after promotion, but was evicted")
453+
}
454+
// /b should have been evicted (oldest untouched).
455+
if _, ok := pc.Lookup("/b"); ok {
456+
t.Error("/b should have been evicted but was found")
457+
}
458+
}
459+
460+
func TestPathCache_FlushClearsAll(t *testing.T) {
461+
pc := security.NewPathCache(16)
462+
for i := 0; i < 10; i++ {
463+
pc.Store(fmt.Sprintf("/k%d", i), fmt.Sprintf("/v%d", i))
464+
}
465+
if pc.Len() != 10 {
466+
t.Fatalf("Len() = %d before Flush, want 10", pc.Len())
467+
}
468+
469+
pc.Flush()
470+
471+
if pc.Len() != 0 {
472+
t.Errorf("Len() = %d after Flush, want 0", pc.Len())
473+
}
474+
if _, ok := pc.Lookup("/k0"); ok {
475+
t.Error("Lookup should miss after Flush")
476+
}
477+
}
478+
479+
func TestPathCache_DefaultSizeOnZero(t *testing.T) {
480+
// Passing 0 should fall back to DefaultPathCacheSize.
481+
pc := security.NewPathCache(0)
482+
// We can't easily assert the internal capacity, but we can verify it
483+
// accepts at least DefaultPathCacheSize entries without panicking and
484+
// that Len() grows correctly.
485+
for i := 0; i < security.DefaultPathCacheSize; i++ {
486+
pc.Store(fmt.Sprintf("/%d", i), fmt.Sprintf("/v/%d", i))
487+
}
488+
if pc.Len() != security.DefaultPathCacheSize {
489+
t.Errorf("Len() = %d, want %d", pc.Len(), security.DefaultPathCacheSize)
490+
}
491+
}
492+
386493
// ---------------------------------------------------------------------------
387494
// Additional PathSafe edge cases
388495
// ---------------------------------------------------------------------------

internal/server/server_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,106 @@ func TestRedirectAuthorityRejectsInvalidConfiguredHost(t *testing.T) {
9797
t.Fatal("redirectAuthority accepted invalid configured host")
9898
}
9999
}
100+
101+
// ---------------------------------------------------------------------------
102+
// Security-hardening defaults on fasthttp.Server (SEC-007, SEC-014, SEC-015)
103+
// ---------------------------------------------------------------------------
104+
105+
func TestNew_HTTPOnly_SecurityDefaults(t *testing.T) {
106+
cfg := &config.ServerConfig{
107+
Addr: ":8080",
108+
MaxConnsPerIP: 50,
109+
}
110+
handler := func(ctx *fasthttp.RequestCtx) {
111+
ctx.SetStatusCode(fasthttp.StatusOK)
112+
}
113+
114+
s := New(cfg, nil, handler)
115+
116+
// SEC-007: Server name must be empty to suppress identity disclosure.
117+
if s.http.Name != "" {
118+
t.Errorf("HTTP Name = %q, want empty (SEC-007)", s.http.Name)
119+
}
120+
121+
// SEC-014: MaxRequestBodySize must be 1024 (small explicit limit).
122+
if s.http.MaxRequestBodySize != 1024 {
123+
t.Errorf("HTTP MaxRequestBodySize = %d, want 1024 (SEC-014)", s.http.MaxRequestBodySize)
124+
}
125+
126+
// SEC-015: MaxConnsPerIP must match configuration.
127+
if s.http.MaxConnsPerIP != 50 {
128+
t.Errorf("HTTP MaxConnsPerIP = %d, want 50 (SEC-015)", s.http.MaxConnsPerIP)
129+
}
130+
131+
// No HTTPS server when TLS is not configured.
132+
if s.https != nil {
133+
t.Error("https server should be nil when TLS is not configured")
134+
}
135+
}
136+
137+
func TestNew_TLS_SecurityDefaults(t *testing.T) {
138+
cfg := &config.ServerConfig{
139+
Addr: ":8080",
140+
TLSAddr: ":8443",
141+
TLSCert: "dummy.crt",
142+
TLSKey: "dummy.key",
143+
RedirectHost: "example.com",
144+
MaxConnsPerIP: 100,
145+
}
146+
handler := func(ctx *fasthttp.RequestCtx) {
147+
ctx.SetStatusCode(fasthttp.StatusOK)
148+
}
149+
150+
s := New(cfg, nil, handler)
151+
152+
// --- HTTP server (redirect handler) ---
153+
154+
if s.http.Name != "" {
155+
t.Errorf("HTTP Name = %q, want empty (SEC-007)", s.http.Name)
156+
}
157+
if s.http.MaxRequestBodySize != 1024 {
158+
t.Errorf("HTTP MaxRequestBodySize = %d, want 1024 (SEC-014)", s.http.MaxRequestBodySize)
159+
}
160+
if s.http.MaxConnsPerIP != 100 {
161+
t.Errorf("HTTP MaxConnsPerIP = %d, want 100 (SEC-015)", s.http.MaxConnsPerIP)
162+
}
163+
164+
// --- HTTPS server ---
165+
166+
if s.https == nil {
167+
t.Fatal("https server should not be nil when TLS is configured")
168+
}
169+
if s.https.Name != "" {
170+
t.Errorf("HTTPS Name = %q, want empty (SEC-007)", s.https.Name)
171+
}
172+
if s.https.MaxRequestBodySize != 1024 {
173+
t.Errorf("HTTPS MaxRequestBodySize = %d, want 1024 (SEC-014)", s.https.MaxRequestBodySize)
174+
}
175+
if s.https.MaxConnsPerIP != 100 {
176+
t.Errorf("HTTPS MaxConnsPerIP = %d, want 100 (SEC-015)", s.https.MaxConnsPerIP)
177+
}
178+
179+
// TLS config must be present with minimum TLS 1.2.
180+
if s.https.TLSConfig == nil {
181+
t.Fatal("HTTPS TLSConfig should not be nil")
182+
}
183+
if s.https.TLSConfig.MinVersion != 0x0303 { // tls.VersionTLS12
184+
t.Errorf("HTTPS TLS MinVersion = %#x, want %#x (TLS 1.2)", s.https.TLSConfig.MinVersion, 0x0303)
185+
}
186+
}
187+
188+
func TestNew_MaxConnsPerIP_Zero(t *testing.T) {
189+
cfg := &config.ServerConfig{
190+
Addr: ":8080",
191+
MaxConnsPerIP: 0, // disabled
192+
}
193+
handler := func(ctx *fasthttp.RequestCtx) {
194+
ctx.SetStatusCode(fasthttp.StatusOK)
195+
}
196+
197+
s := New(cfg, nil, handler)
198+
199+
if s.http.MaxConnsPerIP != 0 {
200+
t.Errorf("HTTP MaxConnsPerIP = %d, want 0 (disabled)", s.http.MaxConnsPerIP)
201+
}
202+
}

0 commit comments

Comments
 (0)