mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-08 23:39:46 +00:00
23cd45a3d7
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
230 lines
7.6 KiB
Go
230 lines
7.6 KiB
Go
package ui
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestNewHTTPLogState tests the constructor
|
|
func TestNewHTTPLogState(t *testing.T) {
|
|
state := newHTTPLogState("forward-123", "my-service")
|
|
|
|
assert.Equal(t, "forward-123", state.forwardID)
|
|
assert.Equal(t, "my-service", state.forwardAlias)
|
|
assert.NotNil(t, state.entries)
|
|
assert.Empty(t, state.entries)
|
|
assert.True(t, state.autoScroll)
|
|
assert.Equal(t, HTTPLogFilterNone, state.filterMode)
|
|
assert.Empty(t, state.filterText)
|
|
assert.False(t, state.filterActive)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_NoFilter tests filtering with no filter
|
|
func TestHTTPLogState_GetFilteredEntries_NoFilter(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
{Method: "GET", Path: "/health", StatusCode: 200},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 3)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_FiltersZeroStatusCode tests that entries without status codes are filtered
|
|
func TestHTTPLogState_GetFilteredEntries_FiltersZeroStatusCode(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/streaming", StatusCode: 0}, // No status (in-progress or error)
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 2)
|
|
assert.Equal(t, "/api/users", filtered[0].Path)
|
|
assert.Equal(t, "/api/orders", filtered[1].Path)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_Non200Filter tests non-2xx filter
|
|
func TestHTTPLogState_GetFilteredEntries_Non200Filter(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterMode = HTTPLogFilterNon200
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/api/error", StatusCode: 500},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
{Method: "GET", Path: "/api/notfound", StatusCode: 404},
|
|
{Method: "PUT", Path: "/api/redirect", StatusCode: 301},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 3)
|
|
assert.Equal(t, 500, filtered[0].StatusCode)
|
|
assert.Equal(t, 404, filtered[1].StatusCode)
|
|
assert.Equal(t, 301, filtered[2].StatusCode)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_ErrorsFilter tests 4xx/5xx filter
|
|
func TestHTTPLogState_GetFilteredEntries_ErrorsFilter(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterMode = HTTPLogFilterErrors
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/api/error", StatusCode: 500},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
{Method: "GET", Path: "/api/notfound", StatusCode: 404},
|
|
{Method: "PUT", Path: "/api/redirect", StatusCode: 301},
|
|
{Method: "GET", Path: "/api/bad", StatusCode: 400},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 3)
|
|
assert.Equal(t, 500, filtered[0].StatusCode)
|
|
assert.Equal(t, 404, filtered[1].StatusCode)
|
|
assert.Equal(t, 400, filtered[2].StatusCode)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_TextFilter tests text filtering
|
|
func TestHTTPLogState_GetFilteredEntries_TextFilter(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterText = "users"
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/api/users/123", StatusCode: 200},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
{Method: "GET", Path: "/health", StatusCode: 200},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 2)
|
|
assert.Equal(t, "/api/users", filtered[0].Path)
|
|
assert.Equal(t, "/api/users/123", filtered[1].Path)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_TextFilterCaseInsensitive tests case-insensitive text filtering
|
|
func TestHTTPLogState_GetFilteredEntries_TextFilterCaseInsensitive(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterText = "API"
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/Api/Orders", StatusCode: 200},
|
|
{Method: "GET", Path: "/health", StatusCode: 200},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 2)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_TextFilterByMethod tests filtering by HTTP method
|
|
func TestHTTPLogState_GetFilteredEntries_TextFilterByMethod(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterText = "POST"
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
{Method: "POST", Path: "/api/items", StatusCode: 201},
|
|
{Method: "PUT", Path: "/api/update", StatusCode: 200},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 2)
|
|
assert.Equal(t, "POST", filtered[0].Method)
|
|
assert.Equal(t, "POST", filtered[1].Method)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_CombinedFilters tests combining mode and text filters
|
|
func TestHTTPLogState_GetFilteredEntries_CombinedFilters(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterMode = HTTPLogFilterErrors
|
|
state.filterText = "api"
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "GET", Path: "/api/error", StatusCode: 500},
|
|
{Method: "GET", Path: "/health", StatusCode: 500}, // Error but doesn't match text
|
|
{Method: "GET", Path: "/api/notfound", StatusCode: 404},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 2)
|
|
assert.Equal(t, "/api/error", filtered[0].Path)
|
|
assert.Equal(t, "/api/notfound", filtered[1].Path)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilteredEntries_EmptyResult tests when no entries match
|
|
func TestHTTPLogState_GetFilteredEntries_EmptyResult(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterText = "nonexistent"
|
|
state.entries = []HTTPLogEntry{
|
|
{Method: "GET", Path: "/api/users", StatusCode: 200},
|
|
{Method: "POST", Path: "/api/orders", StatusCode: 201},
|
|
}
|
|
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Empty(t, filtered)
|
|
}
|
|
|
|
// TestHTTPLogState_GetFilterModeLabel tests filter mode labels
|
|
func TestHTTPLogState_GetFilterModeLabel(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
|
|
tests := []struct {
|
|
mode HTTPLogFilterMode
|
|
expected string
|
|
}{
|
|
{HTTPLogFilterNone, "All"},
|
|
{HTTPLogFilterText, "Text"},
|
|
{HTTPLogFilterNon200, "Non-2xx"},
|
|
{HTTPLogFilterErrors, "Errors (4xx/5xx)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
state.filterMode = tt.mode
|
|
assert.Equal(t, tt.expected, state.getFilterModeLabel())
|
|
}
|
|
}
|
|
|
|
// TestHTTPLogState_FilterModeValues tests filter mode constants are correct
|
|
func TestHTTPLogState_FilterModeValues(t *testing.T) {
|
|
// Ensure the modes are sequential for cycling to work correctly
|
|
assert.Equal(t, HTTPLogFilterMode(0), HTTPLogFilterNone)
|
|
assert.Equal(t, HTTPLogFilterMode(1), HTTPLogFilterText)
|
|
assert.Equal(t, HTTPLogFilterMode(2), HTTPLogFilterNon200)
|
|
assert.Equal(t, HTTPLogFilterMode(3), HTTPLogFilterErrors)
|
|
}
|
|
|
|
// TestHTTPLogState_LargeEntrySet tests filtering performance with many entries
|
|
func TestHTTPLogState_LargeEntrySet(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
|
|
// Add 1000 entries
|
|
for i := 0; i < 1000; i++ {
|
|
code := 200
|
|
if i%10 == 0 {
|
|
code = 500
|
|
}
|
|
state.entries = append(state.entries, HTTPLogEntry{
|
|
Method: "GET",
|
|
Path: "/api/test",
|
|
StatusCode: code,
|
|
})
|
|
}
|
|
|
|
// Filter should work correctly
|
|
state.filterMode = HTTPLogFilterErrors
|
|
filtered := state.getFilteredEntries()
|
|
|
|
assert.Len(t, filtered, 100) // 10% are errors
|
|
}
|