yggdrasil-go/src/webui/error_handling_test.go
Andy Oknen 113dcbb72a Add password authentication to WebUI and implement session management
- Updated WebUI configuration to include a password field for authentication.
- Enhanced the WebUI server to handle login and logout functionality with session management.
- Added tests for authentication and session handling.
- Updated README and example configuration to reflect new authentication features.
2025-07-30 08:34:29 +00:00

364 lines
8.7 KiB
Go

package webui
import (
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestWebUIServer_InvalidListenAddress(t *testing.T) {
logger := createTestLogger()
// Test various invalid listen addresses
testCases := []struct {
addr string
shouldFail bool
description string
}{
{"invalid:address", true, "Invalid address format"},
{"256.256.256.256:8080", true, "Invalid IP address"},
{"localhost:-1", true, "Negative port"},
{"localhost:99999", true, "Port out of range"},
{"not-a-valid-address", true, "Completely invalid address"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Address_%s_%s", tc.addr, tc.description), func(t *testing.T) {
server := Server(tc.addr, "", logger)
// Use a timeout to prevent hanging on addresses that might partially work
done := make(chan error, 1)
go func() {
done <- server.Start()
}()
select {
case err := <-done:
if tc.shouldFail && err == nil {
_ = server.Stop() // Clean up if it somehow started
t.Errorf("Expected Start() to fail for invalid address %s", tc.addr)
} else if !tc.shouldFail && err != nil {
t.Errorf("Expected Start() to succeed for address %s, got error: %v", tc.addr, err)
}
case <-time.After(2 * time.Second):
// If it times out, the server might be listening, stop it
_ = server.Stop()
if tc.shouldFail {
t.Errorf("Start() did not fail quickly enough for invalid address %s", tc.addr)
} else {
t.Logf("Start() timed out for address %s, assuming it started successfully", tc.addr)
}
}
})
}
}
func TestWebUIServer_PortAlreadyInUse(t *testing.T) {
logger := createTestLogger()
// Start a server on a specific port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
// Get the port that's now in use
usedPort := listener.Addr().(*net.TCPAddr).Port
conflictAddress := fmt.Sprintf("127.0.0.1:%d", usedPort)
server := Server(conflictAddress, "", logger)
// This should fail because port is already in use
err = server.Start()
if err == nil {
_ = server.Stop()
t.Error("Expected Start() to fail when port is already in use")
}
}
func TestWebUIServer_DoubleStart(t *testing.T) {
logger := createTestLogger()
// Create two separate servers to test behavior
server1 := Server("127.0.0.1:0", "", logger)
server2 := Server("127.0.0.1:0", "", logger)
// Start first server
startDone1 := make(chan error, 1)
go func() {
startDone1 <- server1.Start()
}()
// Wait for first server to start
time.Sleep(100 * time.Millisecond)
if server1.server == nil {
t.Fatal("First server should have started")
}
// Start second server (should work since different instance)
startDone2 := make(chan error, 1)
go func() {
startDone2 <- server2.Start()
}()
// Wait a bit then stop both servers
time.Sleep(100 * time.Millisecond)
err1 := server1.Stop()
if err1 != nil {
t.Errorf("Stop() failed for server1: %v", err1)
}
err2 := server2.Stop()
if err2 != nil {
t.Errorf("Stop() failed for server2: %v", err2)
}
// Wait for both Start() calls to complete
select {
case err := <-startDone1:
if err != nil {
t.Logf("First Start() returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Error("First Start() did not return after Stop()")
}
select {
case err := <-startDone2:
if err != nil {
t.Logf("Second Start() returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Error("Second Start() did not return after Stop()")
}
}
func TestWebUIServer_StopTwice(t *testing.T) {
logger := createTestLogger()
server := Server("127.0.0.1:0", "", logger)
// Start server
go func() {
_ = server.Start()
}()
time.Sleep(100 * time.Millisecond)
// Stop server first time
err := server.Stop()
if err != nil {
t.Errorf("First Stop() failed: %v", err)
}
// Stop server second time - should not error
err = server.Stop()
if err != nil {
t.Errorf("Second Stop() failed: %v", err)
}
}
func TestWebUIServer_GracefulShutdown(t *testing.T) {
logger := createTestLogger()
// Create a listener to get a real address
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
addr := listener.Addr().String()
listener.Close() // Close so our server can use it
server := Server(addr, "", logger)
// Channel to track when Start() returns
startDone := make(chan error, 1)
// Start server
go func() {
startDone <- server.Start()
}()
time.Sleep(100 * time.Millisecond)
// Verify server is running
if server.server == nil {
t.Fatal("Server should be running")
}
// Make a request while server is running
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://%s/health", addr))
if err != nil {
t.Fatalf("Request failed while server running: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
// Stop server
err = server.Stop()
if err != nil {
t.Errorf("Stop() failed: %v", err)
}
// Verify Start() returns
select {
case err := <-startDone:
if err != nil {
t.Errorf("Start() returned error after Stop(): %v", err)
}
case <-time.After(2 * time.Second):
t.Error("Start() did not return within timeout after Stop()")
}
// Verify server is no longer accessible
_, err = client.Get(fmt.Sprintf("http://%s/health", addr))
if err == nil {
t.Error("Expected request to fail after server stopped")
}
}
func TestWebUIServer_ContextCancellation(t *testing.T) {
logger := createTestLogger()
server := Server("127.0.0.1:0", "", logger)
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
// Start server
go func() {
_ = server.Start()
}()
time.Sleep(100 * time.Millisecond)
// Wait for context to be cancelled
<-ctx.Done()
// Stop server after context cancellation
err := server.Stop()
if err != nil {
t.Errorf("Stop() failed after context cancellation: %v", err)
}
}
func TestWebUIServer_LoggerNil(t *testing.T) {
// Test server creation with nil logger
server := Server("127.0.0.1:0", "", nil)
if server == nil {
t.Fatal("Server should be created even with nil logger")
}
if server.log != nil {
t.Error("Server logger should be nil if nil was passed")
}
}
func TestWebUIServer_RapidStartStop(t *testing.T) {
logger := createTestLogger()
// Test rapid start/stop cycles with fewer iterations
for i := 0; i < 5; i++ {
server := Server("127.0.0.1:0", "", logger)
// Start server
startDone := make(chan error, 1)
go func() {
startDone <- server.Start()
}()
// Wait a bit for server to start
time.Sleep(50 * time.Millisecond)
// Stop server
err := server.Stop()
if err != nil {
t.Errorf("Iteration %d: Stop() failed: %v", i, err)
}
// Wait for Start() to return
select {
case <-startDone:
// Start() returned, good
case <-time.After(1 * time.Second):
t.Errorf("Iteration %d: Start() did not return after Stop()", i)
}
// Pause between iterations to avoid port conflicts
time.Sleep(50 * time.Millisecond)
}
}
func TestWebUIServer_LargeNumberOfRequests(t *testing.T) {
logger := createTestLogger()
// Use httptest.Server for more reliable testing
mux := http.NewServeMux()
testServer := Server("127.0.0.1:0", "", createTestLogger()); setupStaticHandler(mux, testServer)
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
serveFile(rw, r, logger)
})
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte("OK"))
})
server := httptest.NewServer(mux)
defer server.Close()
// Send many requests quickly
const numRequests = 50 // Reduced number for more reliable testing
errorChan := make(chan error, numRequests)
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < numRequests; i++ {
go func(requestID int) {
resp, err := client.Get(server.URL + "/health")
if err != nil {
errorChan <- fmt.Errorf("request %d failed: %v", requestID, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
errorChan <- fmt.Errorf("request %d: expected status 200, got %d", requestID, resp.StatusCode)
return
}
errorChan <- nil
}(i)
}
// Check results
errorCount := 0
for i := 0; i < numRequests; i++ {
select {
case err := <-errorChan:
if err != nil {
errorCount++
if errorCount <= 5 { // Only log first few errors
t.Errorf("Request error: %v", err)
}
}
case <-time.After(10 * time.Second):
t.Fatalf("Request %d timed out", i)
}
}
if errorCount > 0 {
t.Errorf("Total failed requests: %d/%d", errorCount, numRequests)
}
}