mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-25 16:35:07 +03:00
Add minimal Web UI server
This commit is contained in:
parent
707e90b1b3
commit
345d5b9cbd
13 changed files with 2058 additions and 0 deletions
335
src/webui/config_test.go
Normal file
335
src/webui/config_test.go
Normal file
|
@ -0,0 +1,335 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||
)
|
||||
|
||||
func TestWebUIConfig_DefaultValues(t *testing.T) {
|
||||
cfg := config.GenerateConfig()
|
||||
|
||||
// Check that WebUI config has reasonable defaults
|
||||
if cfg.WebUI.Port == 0 {
|
||||
t.Log("Note: WebUI Port is 0 (might be default unset value)")
|
||||
}
|
||||
|
||||
// Host can be empty (meaning all interfaces)
|
||||
if cfg.WebUI.Host == "" {
|
||||
t.Log("Note: WebUI Host is empty (binds to all interfaces)")
|
||||
}
|
||||
|
||||
// Enable should have a default value
|
||||
if !cfg.WebUI.Enable && cfg.WebUI.Enable {
|
||||
t.Log("WebUI Enable flag has a boolean value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_Validation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config config.WebUIConfig
|
||||
valid bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Valid config with default port",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 9000,
|
||||
Host: "",
|
||||
},
|
||||
valid: true,
|
||||
expected: ":9000",
|
||||
},
|
||||
{
|
||||
name: "Valid config with localhost",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 8080,
|
||||
Host: "localhost",
|
||||
},
|
||||
valid: true,
|
||||
expected: "localhost:8080",
|
||||
},
|
||||
{
|
||||
name: "Valid config with specific IP",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 3000,
|
||||
Host: "127.0.0.1",
|
||||
},
|
||||
valid: true,
|
||||
expected: "127.0.0.1:3000",
|
||||
},
|
||||
{
|
||||
name: "Valid config with IPv6",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 9000,
|
||||
Host: "::1",
|
||||
},
|
||||
valid: true,
|
||||
expected: "[::1]:9000",
|
||||
},
|
||||
{
|
||||
name: "Disabled config",
|
||||
config: config.WebUIConfig{
|
||||
Enable: false,
|
||||
Port: 9000,
|
||||
Host: "localhost",
|
||||
},
|
||||
valid: false,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Zero port",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 0,
|
||||
Host: "localhost",
|
||||
},
|
||||
valid: true,
|
||||
expected: "localhost:0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test building listen address from config
|
||||
var listenAddr string
|
||||
|
||||
if tc.config.Enable {
|
||||
if tc.config.Host == "" {
|
||||
listenAddr = fmt.Sprintf(":%d", tc.config.Port)
|
||||
} else if tc.config.Host == "::1" || (len(tc.config.Host) > 0 && tc.config.Host[0] == ':') {
|
||||
// IPv6 needs brackets
|
||||
listenAddr = fmt.Sprintf("[%s]:%d", tc.config.Host, tc.config.Port)
|
||||
} else {
|
||||
listenAddr = fmt.Sprintf("%s:%d", tc.config.Host, tc.config.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.valid {
|
||||
if listenAddr != tc.expected {
|
||||
t.Errorf("Expected listen address %s, got %s", tc.expected, listenAddr)
|
||||
}
|
||||
|
||||
// Try to create server with this config
|
||||
logger := createTestLogger()
|
||||
server := Server(listenAddr, logger)
|
||||
if server == nil {
|
||||
t.Error("Failed to create server with valid config")
|
||||
}
|
||||
} else {
|
||||
if tc.config.Enable {
|
||||
t.Error("Config should be considered invalid when WebUI is disabled")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_PortRanges(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test various port ranges
|
||||
portTests := []struct {
|
||||
port uint16
|
||||
shouldWork bool
|
||||
description string
|
||||
}{
|
||||
{1, true, "Port 1 (lowest valid port)"},
|
||||
{80, true, "Port 80 (HTTP)"},
|
||||
{443, true, "Port 443 (HTTPS)"},
|
||||
{8080, true, "Port 8080 (common alternative)"},
|
||||
{9000, true, "Port 9000 (default WebUI)"},
|
||||
{65535, true, "Port 65535 (highest valid port)"},
|
||||
{0, true, "Port 0 (OS assigns port)"},
|
||||
}
|
||||
|
||||
for _, test := range portTests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
listenAddr := fmt.Sprintf("127.0.0.1:%d", test.port)
|
||||
server := Server(listenAddr, logger)
|
||||
|
||||
if server == nil {
|
||||
t.Errorf("Failed to create server for %s", test.description)
|
||||
return
|
||||
}
|
||||
|
||||
// For port 0, the OS will assign an available port
|
||||
// For other ports, we just check if server creation succeeds
|
||||
if test.shouldWork {
|
||||
// Try to start briefly to see if port is valid
|
||||
go func() {
|
||||
server.Start()
|
||||
}()
|
||||
|
||||
// Quick cleanup
|
||||
server.Stop()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_HostFormats(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
hostTests := []struct {
|
||||
host string
|
||||
port uint16
|
||||
expected string
|
||||
description string
|
||||
}{
|
||||
{"", 9000, ":9000", "Empty host (all interfaces)"},
|
||||
{"localhost", 9000, "localhost:9000", "Localhost"},
|
||||
{"127.0.0.1", 9000, "127.0.0.1:9000", "IPv4 loopback"},
|
||||
{"0.0.0.0", 9000, "0.0.0.0:9000", "IPv4 all interfaces"},
|
||||
{"::1", 9000, "[::1]:9000", "IPv6 loopback"},
|
||||
{"::", 9000, "[::]:9000", "IPv6 all interfaces"},
|
||||
}
|
||||
|
||||
for _, test := range hostTests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
var listenAddr string
|
||||
|
||||
if test.host == "" {
|
||||
listenAddr = fmt.Sprintf(":%d", test.port)
|
||||
} else if test.host == "::1" || test.host == "::" {
|
||||
listenAddr = fmt.Sprintf("[%s]:%d", test.host, test.port)
|
||||
} else {
|
||||
listenAddr = fmt.Sprintf("%s:%d", test.host, test.port)
|
||||
}
|
||||
|
||||
if listenAddr != test.expected {
|
||||
t.Errorf("Expected %s, got %s", test.expected, listenAddr)
|
||||
}
|
||||
|
||||
server := Server(listenAddr, logger)
|
||||
if server == nil {
|
||||
t.Errorf("Failed to create server with %s", test.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_Integration(t *testing.T) {
|
||||
// Test integration with actual config generation
|
||||
cfg := config.GenerateConfig()
|
||||
|
||||
// Modify WebUI config
|
||||
cfg.WebUI.Enable = true
|
||||
cfg.WebUI.Port = 9001
|
||||
cfg.WebUI.Host = "127.0.0.1"
|
||||
|
||||
// Build listen address from config
|
||||
listenAddr := fmt.Sprintf("%s:%d", cfg.WebUI.Host, cfg.WebUI.Port)
|
||||
|
||||
logger := createTestLogger()
|
||||
server := Server(listenAddr, logger)
|
||||
|
||||
if server == nil {
|
||||
t.Fatal("Failed to create server from generated config")
|
||||
}
|
||||
|
||||
// Test that server can start with this config
|
||||
go func() {
|
||||
server.Start()
|
||||
}()
|
||||
defer server.Stop()
|
||||
|
||||
// Verify server properties match config
|
||||
if server.listen != listenAddr {
|
||||
t.Errorf("Server listen address %s doesn't match config %s", server.listen, listenAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_JSONSerialization(t *testing.T) {
|
||||
// Test that WebUIConfig can be serialized/deserialized
|
||||
// This is important for config file handling
|
||||
|
||||
originalConfig := config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 8080,
|
||||
Host: "localhost",
|
||||
}
|
||||
|
||||
// In a real scenario, this would go through JSON marshaling/unmarshaling
|
||||
// For this test, we'll just verify the struct is properly defined
|
||||
|
||||
if originalConfig.Enable != true {
|
||||
t.Error("Enable field not properly set")
|
||||
}
|
||||
|
||||
if originalConfig.Port != 8080 {
|
||||
t.Error("Port field not properly set")
|
||||
}
|
||||
|
||||
if originalConfig.Host != "localhost" {
|
||||
t.Error("Host field not properly set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIConfig_EdgeCases(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test edge cases for configuration
|
||||
edgeCases := []struct {
|
||||
name string
|
||||
config config.WebUIConfig
|
||||
test func(t *testing.T, cfg config.WebUIConfig)
|
||||
}{
|
||||
{
|
||||
name: "All zeros",
|
||||
config: config.WebUIConfig{
|
||||
Enable: false,
|
||||
Port: 0,
|
||||
Host: "",
|
||||
},
|
||||
test: func(t *testing.T, cfg config.WebUIConfig) {
|
||||
if cfg.Enable {
|
||||
t.Error("Enable should be false")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Maximum port",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 65535,
|
||||
Host: "127.0.0.1",
|
||||
},
|
||||
test: func(t *testing.T, cfg config.WebUIConfig) {
|
||||
listenAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
server := Server(listenAddr, logger)
|
||||
if server == nil {
|
||||
t.Error("Should be able to create server with max port")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unicode host (should be handled gracefully)",
|
||||
config: config.WebUIConfig{
|
||||
Enable: true,
|
||||
Port: 9000,
|
||||
Host: "тест",
|
||||
},
|
||||
test: func(t *testing.T, cfg config.WebUIConfig) {
|
||||
listenAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
server := Server(listenAddr, logger)
|
||||
// Server creation should not panic, even with invalid host
|
||||
if server == nil {
|
||||
t.Error("Server creation should not fail due to host format")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range edgeCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.test(t, tc.config)
|
||||
})
|
||||
}
|
||||
}
|
282
src/webui/endpoints_test.go
Normal file
282
src/webui/endpoints_test.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWebUIServer_RootEndpoint(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Use httptest.Server for more reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
serveFile(rw, r, logger)
|
||||
})
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
// Test root endpoint
|
||||
resp, err := http.Get(server.URL + "/")
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting root endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return some content (index.html or 404, depending on build mode)
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 200 or 404, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_HealthEndpointDetails(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Use httptest.Server for more reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test health endpoint with different HTTP methods
|
||||
testCases := []struct {
|
||||
method string
|
||||
expectedStatus int
|
||||
}{
|
||||
{"GET", http.StatusOK},
|
||||
{"POST", http.StatusOK},
|
||||
{"PUT", http.StatusOK},
|
||||
{"DELETE", http.StatusOK},
|
||||
{"HEAD", http.StatusOK},
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Method_%s", tc.method), func(t *testing.T) {
|
||||
req, err := http.NewRequest(tc.method, server.URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tc.expectedStatus {
|
||||
t.Errorf("Expected status %d for %s, got %d", tc.expectedStatus, tc.method, resp.StatusCode)
|
||||
}
|
||||
|
||||
if tc.method != "HEAD" {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if string(body) != "OK" {
|
||||
t.Errorf("Expected body 'OK', got '%s'", string(body))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_NonExistentEndpoint(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Use httptest.Server for more reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test non-existent endpoints
|
||||
testPaths := []string{
|
||||
"/nonexistent",
|
||||
"/api/v1/test",
|
||||
"/static/nonexistent.css",
|
||||
"/admin",
|
||||
"/config",
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for _, path := range testPaths {
|
||||
t.Run(fmt.Sprintf("Path_%s", strings.ReplaceAll(path, "/", "_")), func(t *testing.T) {
|
||||
resp, err := client.Get(server.URL + path)
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting %s: %v", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return 404 for non-existent paths
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for %s, got %d", path, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_ContentTypes(t *testing.T) {
|
||||
// This test checks if proper content types are set
|
||||
// We'll use httptest.Server for more controlled testing
|
||||
|
||||
logger := createTestLogger()
|
||||
|
||||
// Create a test handler similar to what the webui server creates
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Setup handlers like in the actual server
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test health endpoint content type
|
||||
resp, err := http.Get(server.URL + "/health")
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting health endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Health endpoint might not set explicit content type, which is fine
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for health endpoint, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_HeaderSecurity(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Use httptest.Server for more reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test that server handles large headers properly
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", server.URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Add a reasonably sized header
|
||||
req.Header.Set("X-Test-Header", strings.Repeat("a", 1000))
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error making request with large header: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 with normal header, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_ConcurrentRequests(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Use httptest.Server for more reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test concurrent requests to health endpoint
|
||||
const numRequests = 20
|
||||
errChan := make(chan error, numRequests)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go func() {
|
||||
resp, err := client.Get(server.URL + "/health")
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errChan <- fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if string(body) != "OK" {
|
||||
errChan <- fmt.Errorf("unexpected body: %s", string(body))
|
||||
return
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all requests to complete
|
||||
for i := 0; i < numRequests; i++ {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
t.Errorf("Concurrent request %d failed: %v", i+1, err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("Request %d timed out", i+1)
|
||||
}
|
||||
}
|
||||
}
|
360
src/webui/error_handling_test.go
Normal file
360
src/webui/error_handling_test.go
Normal file
|
@ -0,0 +1,360 @@
|
|||
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
|
||||
invalidAddresses := []string{
|
||||
"invalid:address",
|
||||
"256.256.256.256:8080",
|
||||
"localhost:-1",
|
||||
"localhost:99999",
|
||||
"not-a-valid-address",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, addr := range invalidAddresses {
|
||||
t.Run(fmt.Sprintf("Address_%s", addr), func(t *testing.T) {
|
||||
server := Server(addr, logger)
|
||||
|
||||
// Start should fail for invalid addresses
|
||||
err := server.Start()
|
||||
if err == nil {
|
||||
server.Stop() // Clean up if it somehow started
|
||||
t.Errorf("Expected Start() to fail for invalid address %s", 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_EmptyListenAddress(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test with empty listen address
|
||||
server := Server("", logger)
|
||||
|
||||
// This might fail when trying to start
|
||||
err := server.Start()
|
||||
if err == nil {
|
||||
server.Stop()
|
||||
t.Log("Note: Server started with empty listen address")
|
||||
} else {
|
||||
t.Logf("Expected behavior: Start() failed with empty address: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
setupStaticHandler(mux)
|
||||
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)
|
||||
}
|
||||
}
|
63
src/webui/server.go
Normal file
63
src/webui/server.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
)
|
||||
|
||||
type WebUIServer struct {
|
||||
server *http.Server
|
||||
log core.Logger
|
||||
listen string
|
||||
}
|
||||
|
||||
func Server(listen string, log core.Logger) *WebUIServer {
|
||||
return &WebUIServer{
|
||||
listen: listen,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebUIServer) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Setup static files handler (implementation varies by build)
|
||||
setupStaticHandler(mux)
|
||||
|
||||
// Serve any file by path (implementation varies by build)
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
serveFile(rw, r, w.log)
|
||||
})
|
||||
|
||||
// Health check endpoint
|
||||
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
w.server = &http.Server{
|
||||
Addr: w.listen,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
w.log.Infof("WebUI server starting on %s", w.listen)
|
||||
|
||||
if err := w.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("WebUI server failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebUIServer) Stop() error {
|
||||
if w.server != nil {
|
||||
return w.server.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
51
src/webui/server_dev.go
Normal file
51
src/webui/server_dev.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package webui
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
)
|
||||
|
||||
// setupStaticHandler configures static file serving for development (files from disk)
|
||||
func setupStaticHandler(mux *http.ServeMux) {
|
||||
// Serve static files from disk for development
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("src/webui/static/"))))
|
||||
}
|
||||
|
||||
// serveFile serves any file from disk or returns 404 if not found
|
||||
func serveFile(rw http.ResponseWriter, r *http.Request, log core.Logger) {
|
||||
// Clean the path and remove leading slash
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
|
||||
// If path is empty, serve index.html
|
||||
if requestPath == "" {
|
||||
requestPath = "index.html"
|
||||
}
|
||||
|
||||
// Construct the full path on disk
|
||||
filePath := filepath.Join("src/webui/static", requestPath)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
log.Debugf("File not found: %s", filePath)
|
||||
http.NotFound(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine content type based on file extension
|
||||
contentType := mime.TypeByExtension(filepath.Ext(requestPath))
|
||||
if contentType != "" {
|
||||
rw.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
log.Debugf("Serving file from disk: %s", filePath)
|
||||
http.ServeFile(rw, r, filePath)
|
||||
}
|
64
src/webui/server_prod.go
Normal file
64
src/webui/server_prod.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package webui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
// setupStaticHandler configures static file serving for production (embedded files)
|
||||
func setupStaticHandler(mux *http.ServeMux) {
|
||||
// Get the embedded file system for static files
|
||||
staticFS, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
panic("failed to get embedded static files: " + err.Error())
|
||||
}
|
||||
|
||||
// Serve static files from embedded FS
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||
}
|
||||
|
||||
// serveFile serves any file from embedded files or returns 404 if not found
|
||||
func serveFile(rw http.ResponseWriter, r *http.Request, log core.Logger) {
|
||||
// Clean the path and remove leading slash
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
|
||||
// If path is empty, serve index.html
|
||||
if requestPath == "" {
|
||||
requestPath = "index.html"
|
||||
}
|
||||
|
||||
// Construct the full path within static directory
|
||||
filePath := "static/" + requestPath
|
||||
|
||||
// Try to read the file from embedded FS
|
||||
data, err := staticFiles.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Debugf("File not found: %s", filePath)
|
||||
http.NotFound(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine content type based on file extension
|
||||
contentType := mime.TypeByExtension(filepath.Ext(requestPath))
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// Set headers and serve the file
|
||||
rw.Header().Set("Content-Type", contentType)
|
||||
rw.Write(data)
|
||||
|
||||
log.Debugf("Served file: %s (type: %s)", filePath, contentType)
|
||||
}
|
219
src/webui/server_test.go
Normal file
219
src/webui/server_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gologme/log"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
)
|
||||
|
||||
// Helper function to create a test logger
|
||||
func createTestLogger() core.Logger {
|
||||
return log.New(os.Stderr, "webui_test: ", log.Flags())
|
||||
}
|
||||
|
||||
// Helper function to get available port for testing
|
||||
func getTestAddress() string {
|
||||
return "127.0.0.1:0" // Let OS assign available port
|
||||
}
|
||||
|
||||
// Helper function to wait for server to be ready
|
||||
func waitForServer(url string, timeout time.Duration) error {
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := client.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("server not ready within timeout")
|
||||
}
|
||||
|
||||
func TestWebUIServer_Creation(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
listen := getTestAddress()
|
||||
|
||||
server := Server(listen, logger)
|
||||
|
||||
if server == nil {
|
||||
t.Fatal("Server function returned nil")
|
||||
}
|
||||
|
||||
if server.listen != listen {
|
||||
t.Errorf("Expected listen address %s, got %s", listen, server.listen)
|
||||
}
|
||||
|
||||
if server.log != logger {
|
||||
t.Error("Logger not properly set")
|
||||
}
|
||||
|
||||
if server.server != nil {
|
||||
t.Error("HTTP server should be nil before Start()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_StartStop(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
listen := getTestAddress()
|
||||
|
||||
server := Server(listen, logger)
|
||||
|
||||
// Start server in goroutine
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- server.Start()
|
||||
}()
|
||||
|
||||
// Give server time to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify server is running
|
||||
if server.server == nil {
|
||||
t.Fatal("HTTP server not initialized after Start()")
|
||||
}
|
||||
|
||||
// Stop server
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("Error stopping server: %v", err)
|
||||
}
|
||||
|
||||
// Check that Start() returns without error after Stop()
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
t.Errorf("Start() returned error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("Start() did not return after Stop()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_StopWithoutStart(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
listen := getTestAddress()
|
||||
|
||||
server := Server(listen, logger)
|
||||
|
||||
// Stop server that was never started should not error
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("Stop() on unstarted server returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_HealthEndpoint(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Create a test server using net/http/httptest for reliable testing
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
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()
|
||||
|
||||
// Test health endpoint
|
||||
resp, err := http.Get(server.URL + "/health")
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting health endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if string(body) != "OK" {
|
||||
t.Errorf("Expected body 'OK', got '%s'", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_Timeouts(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
server := Server("127.0.0.1:0", logger)
|
||||
|
||||
// Start server
|
||||
go func() {
|
||||
server.Start()
|
||||
}()
|
||||
defer server.Stop()
|
||||
|
||||
// Wait for server to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if server.server == nil {
|
||||
t.Fatal("Server not started")
|
||||
}
|
||||
|
||||
// Check that timeouts are properly configured
|
||||
expectedReadTimeout := 10 * time.Second
|
||||
expectedWriteTimeout := 10 * time.Second
|
||||
expectedMaxHeaderBytes := 1 << 20
|
||||
|
||||
if server.server.ReadTimeout != expectedReadTimeout {
|
||||
t.Errorf("Expected ReadTimeout %v, got %v", expectedReadTimeout, server.server.ReadTimeout)
|
||||
}
|
||||
|
||||
if server.server.WriteTimeout != expectedWriteTimeout {
|
||||
t.Errorf("Expected WriteTimeout %v, got %v", expectedWriteTimeout, server.server.WriteTimeout)
|
||||
}
|
||||
|
||||
if server.server.MaxHeaderBytes != expectedMaxHeaderBytes {
|
||||
t.Errorf("Expected MaxHeaderBytes %d, got %d", expectedMaxHeaderBytes, server.server.MaxHeaderBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebUIServer_ConcurrentStartStop(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test concurrent start/stop operations with separate servers
|
||||
for i := 0; i < 3; i++ {
|
||||
server := Server("127.0.0.1:0", logger)
|
||||
|
||||
// Start server
|
||||
startDone := make(chan error, 1)
|
||||
go func() {
|
||||
startDone <- server.Start()
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop server
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("Iteration %d: Error stopping server: %v", i, err)
|
||||
}
|
||||
|
||||
// Wait for Start() to return
|
||||
select {
|
||||
case <-startDone:
|
||||
// Good, Start() returned
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Errorf("Iteration %d: Start() did not return after Stop()", i)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
55
src/webui/static/index.html
Normal file
55
src/webui/static/index.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Yggdrasil Web Interface</title>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🌳 Yggdrasil Web Interface</h1>
|
||||
<p>Network mesh management dashboard</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="status-card">
|
||||
<h2>Node Status</h2>
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot active"></span>
|
||||
<span>Active</span>
|
||||
</div>
|
||||
<p>WebUI is running and accessible</p>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Configuration</h3>
|
||||
<p>Manage node settings and peers</p>
|
||||
<small>Coming soon...</small>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<h3>Peers</h3>
|
||||
<p>View and manage peer connections</p>
|
||||
<small>Coming soon...</small>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<h3>Network</h3>
|
||||
<p>Network topology and routing</p>
|
||||
<small>Coming soon...</small>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Yggdrasil Network • Minimal WebUI v1.0</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
136
src/webui/static/style.css
Normal file
136
src/webui/static/style.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
main {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: #28a745;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: #495057;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
color: #6c757d;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-card small {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
186
src/webui/static_files_prod_test.go
Normal file
186
src/webui/static_files_prod_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStaticFiles_ProdMode_EmbeddedFiles(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test that the embedded files system is working
|
||||
// Note: In production mode, we can't easily create test files
|
||||
// so we test the behavior with what's available
|
||||
|
||||
// Test serveFile function with various paths
|
||||
testCases := []struct {
|
||||
path string
|
||||
expectedStatus int
|
||||
description string
|
||||
}{
|
||||
{"/", http.StatusOK, "root path should serve index.html if available"},
|
||||
{"/index.html", http.StatusOK, "index.html should be available if embedded"},
|
||||
{"/style.css", http.StatusOK, "style.css should be available if embedded"},
|
||||
{"/nonexistent.txt", http.StatusNotFound, "non-existent files should return 404"},
|
||||
{"/subdir/nonexistent.html", http.StatusNotFound, "non-existent nested files should return 404"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(strings.ReplaceAll(tc.path, "/", "_"), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tc.path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
// For embedded files, we expect either 200 (if file exists) or 404 (if not)
|
||||
if rec.Code != http.StatusOK && rec.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 200 or 404 for %s, got %d", tc.path, rec.Code)
|
||||
}
|
||||
|
||||
// Check that known files return expected status if they exist
|
||||
if (tc.path == "/" || tc.path == "/index.html") && rec.Code == http.StatusOK {
|
||||
// Should have HTML content type
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if !strings.Contains(contentType, "text/html") {
|
||||
t.Logf("Note: Content-Type for %s is %s (might not contain text/html)", tc.path, contentType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_ProdMode_SetupStaticHandler(t *testing.T) {
|
||||
// Test that setupStaticHandler works in production mode
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// This should not panic
|
||||
setupStaticHandler(mux)
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
// Test static handler route
|
||||
resp, err := http.Get(server.URL + "/static/style.css")
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting static file: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return either 200 (if file exists) or 404 (if not embedded)
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 200 or 404 for static file, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_ProdMode_PathTraversal(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test path traversal attempts in production mode
|
||||
pathTraversalTests := []string{
|
||||
"/../sensitive.txt",
|
||||
"/../../etc/passwd",
|
||||
"/..\\sensitive.txt",
|
||||
"/static/../../../etc/passwd",
|
||||
"/static/../../config.json",
|
||||
}
|
||||
|
||||
for _, path := range pathTraversalTests {
|
||||
t.Run(strings.ReplaceAll(path, "/", "_"), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
// Should return 404 for path traversal attempts
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for path traversal attempt %s, got %d", path, rec.Code)
|
||||
}
|
||||
|
||||
// Should not contain any system file content
|
||||
body := rec.Body.String()
|
||||
if strings.Contains(body, "root:") || strings.Contains(body, "/bin/") {
|
||||
t.Errorf("Path traversal might be successful for %s - system content detected", path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_ProdMode_ContentTypes(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test that proper content types are set for different file types
|
||||
testCases := []struct {
|
||||
path string
|
||||
expectedContentType string
|
||||
}{
|
||||
{"/index.html", "text/html"},
|
||||
{"/style.css", "text/css"},
|
||||
{"/script.js", "text/javascript"},
|
||||
{"/data.json", "application/json"},
|
||||
{"/image.png", "image/png"},
|
||||
{"/favicon.ico", "image/x-icon"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(strings.ReplaceAll(tc.path, "/", "_"), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tc.path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
// Only check content type if file exists (status 200)
|
||||
if rec.Code == http.StatusOK {
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if !strings.Contains(contentType, tc.expectedContentType) {
|
||||
t.Logf("Note: Expected content type %s for %s, got %s", tc.expectedContentType, tc.path, contentType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_ProdMode_EmptyPath(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Test that empty path serves index.html
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
// Should return either 200 (if index.html exists) or 404 (if not embedded)
|
||||
if rec.Code != http.StatusOK && rec.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 200 or 404 for root path, got %d", rec.Code)
|
||||
}
|
||||
|
||||
// If successful, should have appropriate content type
|
||||
if rec.Code == http.StatusOK {
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if contentType == "" {
|
||||
t.Logf("Note: No Content-Type header set for root path")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_ProdMode_EmbeddedFileSystem(t *testing.T) {
|
||||
// Test that the embedded file system can be accessed
|
||||
// This is a basic test to ensure the embed directive works
|
||||
|
||||
// Try to read from embedded FS directly
|
||||
_, err := staticFiles.ReadFile("static/index.html")
|
||||
if err != nil {
|
||||
// This is expected if the file doesn't exist in embedded FS
|
||||
t.Logf("Note: index.html not found in embedded FS: %v", err)
|
||||
}
|
||||
|
||||
// Test that we can at least access the embedded FS without panic
|
||||
_, err = staticFiles.ReadFile("static/nonexistent.txt")
|
||||
if err == nil {
|
||||
t.Error("Expected error when reading non-existent file from embedded FS")
|
||||
}
|
||||
}
|
272
src/webui/static_files_test.go
Normal file
272
src/webui/static_files_test.go
Normal file
|
@ -0,0 +1,272 @@
|
|||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStaticFiles_DevMode_ServeFile(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Create temporary test files
|
||||
tempDir := t.TempDir()
|
||||
staticDir := filepath.Join(tempDir, "src", "webui", "static")
|
||||
err := os.MkdirAll(staticDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp static dir: %v", err)
|
||||
}
|
||||
|
||||
// Create test files
|
||||
testFiles := map[string]string{
|
||||
"index.html": "<html><body>Test Index</body></html>",
|
||||
"style.css": "body { background: white; }",
|
||||
"script.js": "console.log('test');",
|
||||
"image.png": "fake png data",
|
||||
"data.json": `{"test": "data"}`,
|
||||
"favicon.ico": "fake ico data",
|
||||
}
|
||||
|
||||
for filename, content := range testFiles {
|
||||
filePath := filepath.Join(staticDir, filename)
|
||||
err := os.WriteFile(filePath, []byte(content), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test file %s: %v", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Change working directory temporarily
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get working directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change working directory: %v", err)
|
||||
}
|
||||
|
||||
// Test serveFile function
|
||||
testCases := []struct {
|
||||
path string
|
||||
expectedStatus int
|
||||
expectedContentType string
|
||||
expectedContent string
|
||||
}{
|
||||
{"/", http.StatusOK, "text/html", testFiles["index.html"]},
|
||||
{"/index.html", http.StatusOK, "text/html", testFiles["index.html"]},
|
||||
{"/style.css", http.StatusOK, "text/css", testFiles["style.css"]},
|
||||
{"/script.js", http.StatusOK, "text/javascript", testFiles["script.js"]},
|
||||
{"/data.json", http.StatusOK, "application/json", testFiles["data.json"]},
|
||||
{"/nonexistent.txt", http.StatusNotFound, "", ""},
|
||||
{"/subdir/nonexistent.html", http.StatusNotFound, "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Path_%s", strings.ReplaceAll(tc.path, "/", "_")), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tc.path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
if rec.Code != tc.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tc.expectedStatus, rec.Code)
|
||||
}
|
||||
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if tc.expectedContentType != "" && !strings.Contains(contentType, tc.expectedContentType) {
|
||||
t.Errorf("Expected content type to contain %s, got %s", tc.expectedContentType, contentType)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if body != tc.expectedContent {
|
||||
t.Errorf("Expected body %q, got %q", tc.expectedContent, body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_DevMode_SetupStaticHandler(t *testing.T) {
|
||||
// Create temporary test files for static handler testing
|
||||
tempDir := t.TempDir()
|
||||
staticDir := filepath.Join(tempDir, "src", "webui", "static")
|
||||
err := os.MkdirAll(staticDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp static dir: %v", err)
|
||||
}
|
||||
|
||||
// Create test CSS file
|
||||
cssContent := "body { color: blue; }"
|
||||
cssPath := filepath.Join(staticDir, "test.css")
|
||||
err = os.WriteFile(cssPath, []byte(cssContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test CSS file: %v", err)
|
||||
}
|
||||
|
||||
// Change working directory temporarily
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get working directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change working directory: %v", err)
|
||||
}
|
||||
|
||||
// Create HTTP server with static handler
|
||||
mux := http.NewServeMux()
|
||||
setupStaticHandler(mux)
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
// Test static file serving
|
||||
resp, err := http.Get(server.URL + "/static/test.css")
|
||||
if err != nil {
|
||||
t.Fatalf("Error requesting static file: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if string(body) != cssContent {
|
||||
t.Errorf("Expected CSS content %q, got %q", cssContent, string(body))
|
||||
}
|
||||
|
||||
// Test Content-Type header
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.Contains(contentType, "text/css") {
|
||||
t.Errorf("Expected Content-Type to contain text/css, got %s", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_DevMode_PathTraversal(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Create temporary test setup
|
||||
tempDir := t.TempDir()
|
||||
staticDir := filepath.Join(tempDir, "src", "webui", "static")
|
||||
err := os.MkdirAll(staticDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp static dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a sensitive file outside static directory
|
||||
sensitiveFile := filepath.Join(tempDir, "sensitive.txt")
|
||||
err = os.WriteFile(sensitiveFile, []byte("sensitive data"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create sensitive file: %v", err)
|
||||
}
|
||||
|
||||
// Change working directory temporarily
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get working directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change working directory: %v", err)
|
||||
}
|
||||
|
||||
// Test path traversal attempts
|
||||
pathTraversalTests := []string{
|
||||
"/../sensitive.txt",
|
||||
"/../../sensitive.txt",
|
||||
"/../../../etc/passwd",
|
||||
"/..\\sensitive.txt",
|
||||
"/static/../../../sensitive.txt",
|
||||
}
|
||||
|
||||
for _, path := range pathTraversalTests {
|
||||
t.Run(fmt.Sprintf("PathTraversal_%s", strings.ReplaceAll(path, "/", "_")), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
// Should return 404 for path traversal attempts
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for path traversal attempt %s, got %d", path, rec.Code)
|
||||
}
|
||||
|
||||
// Should not contain sensitive data
|
||||
body := rec.Body.String()
|
||||
if strings.Contains(body, "sensitive data") {
|
||||
t.Errorf("Path traversal successful for %s - sensitive data leaked", path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFiles_DevMode_EmptyPath(t *testing.T) {
|
||||
logger := createTestLogger()
|
||||
|
||||
// Create temporary test setup with index.html
|
||||
tempDir := t.TempDir()
|
||||
staticDir := filepath.Join(tempDir, "src", "webui", "static")
|
||||
err := os.MkdirAll(staticDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp static dir: %v", err)
|
||||
}
|
||||
|
||||
indexContent := "<html><body>Index Page</body></html>"
|
||||
indexPath := filepath.Join(staticDir, "index.html")
|
||||
err = os.WriteFile(indexPath, []byte(indexContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create index.html: %v", err)
|
||||
}
|
||||
|
||||
// Change working directory temporarily
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get working directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change working directory: %v", err)
|
||||
}
|
||||
|
||||
// Test that empty path serves index.html
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveFile(rec, req, logger)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for root path, got %d", rec.Code)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if body != indexContent {
|
||||
t.Errorf("Expected index content %q, got %q", indexContent, body)
|
||||
}
|
||||
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if !strings.Contains(contentType, "text/html") {
|
||||
t.Errorf("Expected Content-Type to contain text/html, got %s", contentType)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue