feat: enhance auth, add input validation and pagination

- Add IP validation in auth handler with detailed logging
- Implement apiKeyMiddleware helper for API key verification
- Add IP and CIDR validation for whitelist operations
- Support pagination for listing temp and permanent entries
- Add parsePagination helper to extract limit and offset
- Include comprehensive unit tests for IsIP, IsCIDR, and IPMatch
This commit is contained in:
db123-test
2026-05-05 11:34:35 +03:30
parent 93c9eb2a45
commit f525eaa8f3
5 changed files with 383 additions and 17 deletions

163
auth_test.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestIsIP(t *testing.T) {
tests := []struct {
name string
input string
wantOK bool
}{
{"ipv4", "192.168.1.1", true},
{"ipv4 loopback", "127.0.0.1", true},
{"ipv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", true},
{"ipv6 short", "::1", true},
{"empty", "", false},
{"hostname", "example.com", false},
{"garbage", "not-an-ip!!", false},
{"partial", "192.168", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsIP(tt.input); got != tt.wantOK {
t.Errorf("IsIP(%q) = %v, want %v", tt.input, got, tt.wantOK)
}
})
}
}
func TestIsCIDR(t *testing.T) {
tests := []struct {
name string
input string
wantOK bool
}{
{"ipv4 cidr", "192.168.1.0/24", true},
{"ipv6 cidr", "2001:db8::/32", true},
{"single host", "10.0.0.0/32", true},
{"plain ip", "10.0.0.1", false},
{"missing prefix", "192.168.1.0", false},
{"garbage", "foo/bar", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsCIDR(tt.input); got != tt.wantOK {
t.Errorf("IsCIDR(%q) = %v, want %v", tt.input, got, tt.wantOK)
}
})
}
}
func TestIPMatchesEntry(t *testing.T) {
tests := []struct {
name string
ip string
entry string
wantOK bool
wantErr bool
}{
{"exact match", "192.168.1.5", "192.168.1.5", true, false},
{"ipv4 cidr match", "10.0.0.5", "10.0.0.0/24", true, false},
{"ipv4 cidr no match", "10.0.1.5", "10.0.0.0/24", false, false},
{"ipv6 cidr match", "2001:db8::1", "2001:db8::/32", true, false},
{"invalid entry", "10.0.0.1", "not-a-cidr", false, false},
{"empty entry", "10.0.0.1", "", false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ipMatchesEntry(tt.ip, tt.entry)
if tt.wantErr {
if err == nil {
t.Errorf("ipMatchesEntry(%q, %q) = %v, want error", tt.ip, tt.entry, got)
}
} else {
if err != nil {
t.Errorf("ipMatchesEntry(%q, %q) error: %v", tt.ip, tt.entry, err)
}
if got != tt.wantOK {
t.Errorf("ipMatchesEntry(%q, %q) = %v, want %v", tt.ip, tt.entry, got, tt.wantOK)
}
}
})
}
}
func TestVerifyAPIKey(t *testing.T) {
token := "super-secret"
tests := []struct {
name string
auth string
wantOK bool
}{
{"correct token", "Bearer super-secret", true},
{"missing Bearer", "super-secret", false},
{"wrong token", "Bearer wrong", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := http.Request{Header: http.Header{}}
r.Header.Set("Authorization", tt.auth)
if got := verifyAPIKey(&r, token); got != tt.wantOK {
t.Errorf("verifyAPIKey(%q) = %v, want %v", tt.auth, got, tt.wantOK)
}
})
}
}
func TestApiKeyMiddleware(t *testing.T) {
token := "test-token"
called := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusNoContent)
})
wrapped := apiKeyMiddleware(next, token)
t.Run("valid key", func(t *testing.T) {
called = false
r := httptest.NewRequest(http.MethodGet, "/test", nil)
r.Header.Set("Authorization", "Bearer test-token")
w := httptest.NewRecorder()
wrapped(w, r)
if !called {
t.Error("middleware did not call next handler")
}
if w.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", w.Code)
}
})
t.Run("invalid key", func(t *testing.T) {
called = false
r := httptest.NewRequest(http.MethodGet, "/test", nil)
r.Header.Set("Authorization", "Bearer wrong")
w := httptest.NewRecorder()
wrapped(w, r)
if called {
t.Error("middleware called next handler despite invalid key")
}
if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", w.Code)
}
})
t.Run("missing key", func(t *testing.T) {
called = false
r := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
wrapped(w, r)
if called {
t.Error("middleware called next handler despite missing key")
}
if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", w.Code)
}
})
}