forked from aegis/pyserveX
go implementation
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// Package routing provides HTTP routing with regex support
|
||||
package routing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/konduktor/konduktor/internal/config"
|
||||
"github.com/konduktor/konduktor/internal/logging"
|
||||
)
|
||||
|
||||
// RouteMatch represents a matched route with captured parameters
|
||||
type RouteMatch struct {
|
||||
Config map[string]interface{}
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// RegexRoute represents a compiled regex route
|
||||
type RegexRoute struct {
|
||||
Pattern *regexp.Regexp
|
||||
Config map[string]interface{}
|
||||
CaseSensitive bool
|
||||
OriginalExpr string
|
||||
}
|
||||
|
||||
// Router handles HTTP routing with exact, regex, and default routes
|
||||
type Router struct {
|
||||
config *config.Config
|
||||
logger *logging.Logger
|
||||
mux *http.ServeMux
|
||||
staticDir string
|
||||
exactRoutes map[string]map[string]interface{}
|
||||
regexRoutes []*RegexRoute
|
||||
defaultRoute map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new router from config
|
||||
func New(cfg *config.Config, logger *logging.Logger) *Router {
|
||||
staticDir := "./static"
|
||||
if cfg != nil && cfg.HTTP.StaticDir != "" {
|
||||
staticDir = cfg.HTTP.StaticDir
|
||||
}
|
||||
|
||||
r := &Router{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
staticDir: staticDir,
|
||||
exactRoutes: make(map[string]map[string]interface{}),
|
||||
regexRoutes: make([]*RegexRoute, 0),
|
||||
}
|
||||
|
||||
r.setupRoutes()
|
||||
return r
|
||||
}
|
||||
|
||||
// NewRouter creates a router without config (for testing)
|
||||
func NewRouter(opts ...RouterOption) *Router {
|
||||
r := &Router{
|
||||
mux: http.NewServeMux(),
|
||||
staticDir: "./static",
|
||||
exactRoutes: make(map[string]map[string]interface{}),
|
||||
regexRoutes: make([]*RegexRoute, 0),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// RouterOption is a functional option for Router
|
||||
type RouterOption func(*Router)
|
||||
|
||||
// WithStaticDir sets the static directory
|
||||
func WithStaticDir(dir string) RouterOption {
|
||||
return func(r *Router) {
|
||||
r.staticDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
// StaticDir returns the static directory path
|
||||
func (r *Router) StaticDir() string {
|
||||
return r.staticDir
|
||||
}
|
||||
|
||||
// Routes returns the regex routes (for testing)
|
||||
func (r *Router) Routes() []*RegexRoute {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.regexRoutes
|
||||
}
|
||||
|
||||
// ExactRoutes returns the exact routes (for testing)
|
||||
func (r *Router) ExactRoutes() map[string]map[string]interface{} {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.exactRoutes
|
||||
}
|
||||
|
||||
// DefaultRoute returns the default route (for testing)
|
||||
func (r *Router) DefaultRoute() map[string]interface{} {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.defaultRoute
|
||||
}
|
||||
|
||||
// AddRoute adds a route with the given pattern and config
|
||||
// Pattern formats:
|
||||
// - "=/path" - exact match
|
||||
// - "~regex" - case-sensitive regex
|
||||
// - "~*regex" - case-insensitive regex
|
||||
// - "__default__" - default/fallback route
|
||||
func (r *Router) AddRoute(pattern string, routeConfig map[string]interface{}) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case pattern == "__default__":
|
||||
r.defaultRoute = routeConfig
|
||||
|
||||
case strings.HasPrefix(pattern, "="):
|
||||
// Exact match route
|
||||
path := strings.TrimPrefix(pattern, "=")
|
||||
r.exactRoutes[path] = routeConfig
|
||||
|
||||
case strings.HasPrefix(pattern, "~*"):
|
||||
// Case-insensitive regex
|
||||
expr := strings.TrimPrefix(pattern, "~*")
|
||||
re, err := regexp.Compile("(?i)" + expr)
|
||||
if err != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Error("Invalid regex pattern", "pattern", pattern, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
r.regexRoutes = append(r.regexRoutes, &RegexRoute{
|
||||
Pattern: re,
|
||||
Config: routeConfig,
|
||||
CaseSensitive: false,
|
||||
OriginalExpr: expr,
|
||||
})
|
||||
|
||||
case strings.HasPrefix(pattern, "~"):
|
||||
// Case-sensitive regex
|
||||
expr := strings.TrimPrefix(pattern, "~")
|
||||
re, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Error("Invalid regex pattern", "pattern", pattern, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
r.regexRoutes = append(r.regexRoutes, &RegexRoute{
|
||||
Pattern: re,
|
||||
Config: routeConfig,
|
||||
CaseSensitive: true,
|
||||
OriginalExpr: expr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Match finds the best matching route for a path
|
||||
// Priority: exact match > regex match > default
|
||||
func (r *Router) Match(path string) *RouteMatch {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// 1. Check exact routes
|
||||
if cfg, ok := r.exactRoutes[path]; ok {
|
||||
return &RouteMatch{
|
||||
Config: cfg,
|
||||
Params: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check regex routes
|
||||
for _, route := range r.regexRoutes {
|
||||
match := route.Pattern.FindStringSubmatch(path)
|
||||
if match != nil {
|
||||
params := make(map[string]string)
|
||||
|
||||
// Extract named groups
|
||||
names := route.Pattern.SubexpNames()
|
||||
for i, name := range names {
|
||||
if i > 0 && name != "" && i < len(match) {
|
||||
params[name] = match[i]
|
||||
}
|
||||
}
|
||||
|
||||
return &RouteMatch{
|
||||
Config: route.Config,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check default route
|
||||
if r.defaultRoute != nil {
|
||||
return &RouteMatch{
|
||||
Config: r.defaultRoute,
|
||||
Params: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupRoutes configures the routes from config
|
||||
func (r *Router) setupRoutes() {
|
||||
// Health check endpoint
|
||||
r.mux.HandleFunc("/health", r.healthHandler)
|
||||
|
||||
// Setup redirect instructions from config
|
||||
if r.config != nil {
|
||||
for from, to := range r.config.Server.RedirectInstructions {
|
||||
fromPath := from
|
||||
toPath := to
|
||||
r.mux.HandleFunc(fromPath, func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, toPath, http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Default handler for all other routes
|
||||
r.mux.HandleFunc("/", r.defaultHandler)
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.mux.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// healthHandler handles health check requests
|
||||
func (r *Router) healthHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// defaultHandler handles requests that don't match other routes
|
||||
func (r *Router) defaultHandler(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
|
||||
// Try to match against configured routes
|
||||
match := r.Match(path)
|
||||
if match != nil {
|
||||
r.handleRouteMatch(w, req, match)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to serve static file
|
||||
if r.staticDir != "" {
|
||||
filePath := filepath.Join(r.staticDir, path)
|
||||
|
||||
// Prevent directory traversal
|
||||
if !strings.HasPrefix(filepath.Clean(filePath), filepath.Clean(r.staticDir)) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
info, err := os.Stat(filePath)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
// Try index.html
|
||||
indexPath := filepath.Join(filePath, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
http.ServeFile(w, req, indexPath)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.ServeFile(w, req, filePath)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
http.NotFound(w, req)
|
||||
}
|
||||
|
||||
// handleRouteMatch handles a matched route
|
||||
func (r *Router) handleRouteMatch(w http.ResponseWriter, req *http.Request, match *RouteMatch) {
|
||||
cfg := match.Config
|
||||
|
||||
// Handle "return" directive
|
||||
if ret, ok := cfg["return"].(string); ok {
|
||||
parts := strings.SplitN(ret, " ", 2)
|
||||
statusCode := 200
|
||||
body := "OK"
|
||||
if len(parts) >= 1 {
|
||||
switch parts[0] {
|
||||
case "200":
|
||||
statusCode = 200
|
||||
case "201":
|
||||
statusCode = 201
|
||||
case "301":
|
||||
statusCode = 301
|
||||
case "302":
|
||||
statusCode = 302
|
||||
case "400":
|
||||
statusCode = 400
|
||||
case "404":
|
||||
statusCode = 404
|
||||
case "500":
|
||||
statusCode = 500
|
||||
}
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
body = parts[1]
|
||||
}
|
||||
|
||||
if ct, ok := cfg["content_type"].(string); ok {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle static files with root
|
||||
if root, ok := cfg["root"].(string); ok {
|
||||
path := req.URL.Path
|
||||
|
||||
if indexFile, ok := cfg["index_file"].(string); ok {
|
||||
if path == "/" || strings.HasSuffix(path, "/") {
|
||||
path = "/" + indexFile
|
||||
}
|
||||
}
|
||||
|
||||
filePath := filepath.Join(root, path)
|
||||
|
||||
if cacheControl, ok := cfg["cache_control"].(string); ok {
|
||||
w.Header().Set("Cache-Control", cacheControl)
|
||||
}
|
||||
|
||||
if headers, ok := cfg["headers"].([]interface{}); ok {
|
||||
for _, h := range headers {
|
||||
if header, ok := h.(string); ok {
|
||||
parts := strings.SplitN(header, ": ", 2)
|
||||
if len(parts) == 2 {
|
||||
w.Header().Set(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.ServeFile(w, req, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle SPA fallback
|
||||
if spaFallback, ok := cfg["spa_fallback"].(bool); ok && spaFallback {
|
||||
root := r.staticDir
|
||||
if rt, ok := cfg["root"].(string); ok {
|
||||
root = rt
|
||||
}
|
||||
|
||||
indexFile := "index.html"
|
||||
if idx, ok := cfg["index_file"].(string); ok {
|
||||
indexFile = idx
|
||||
}
|
||||
|
||||
filePath := filepath.Join(root, indexFile)
|
||||
http.ServeFile(w, req, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, req)
|
||||
}
|
||||
|
||||
// CreateRouterFromConfig creates a router from extension config
|
||||
func CreateRouterFromConfig(cfg map[string]interface{}) *Router {
|
||||
router := NewRouter()
|
||||
|
||||
if locations, ok := cfg["regex_locations"].(map[string]interface{}); ok {
|
||||
for pattern, routeCfg := range locations {
|
||||
if rc, ok := routeCfg.(map[string]interface{}); ok {
|
||||
router.AddRoute(pattern, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ============== Router Initialization Tests ==============
|
||||
|
||||
func TestRouter_Initialization(t *testing.T) {
|
||||
router := NewRouter()
|
||||
|
||||
if router.StaticDir() != "./static" {
|
||||
t.Errorf("Expected static dir ./static, got %s", router.StaticDir())
|
||||
}
|
||||
|
||||
if len(router.Routes()) != 0 {
|
||||
t.Error("Expected empty routes")
|
||||
}
|
||||
|
||||
if len(router.ExactRoutes()) != 0 {
|
||||
t.Error("Expected empty exact routes")
|
||||
}
|
||||
|
||||
if router.DefaultRoute() != nil {
|
||||
t.Error("Expected nil default route")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_CustomStaticDir(t *testing.T) {
|
||||
router := NewRouter(WithStaticDir("/custom/path"))
|
||||
|
||||
if router.StaticDir() != "/custom/path" {
|
||||
t.Errorf("Expected static dir /custom/path, got %s", router.StaticDir())
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Route Adding Tests ==============
|
||||
|
||||
func TestRouter_AddExactRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"return": "200 OK"}
|
||||
|
||||
router.AddRoute("=/health", config)
|
||||
|
||||
exactRoutes := router.ExactRoutes()
|
||||
if _, ok := exactRoutes["/health"]; !ok {
|
||||
t.Error("Expected /health in exact routes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_AddDefaultRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"spa_fallback": true, "root": "./static"}
|
||||
|
||||
router.AddRoute("__default__", config)
|
||||
|
||||
if router.DefaultRoute() == nil {
|
||||
t.Error("Expected default route to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_AddRegexRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"root": "./static"}
|
||||
|
||||
router.AddRoute("~^/api/", config)
|
||||
|
||||
if len(router.Routes()) != 1 {
|
||||
t.Errorf("Expected 1 regex route, got %d", len(router.Routes()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_AddCaseInsensitiveRegexRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"root": "./static", "cache_control": "public, max-age=3600"}
|
||||
|
||||
router.AddRoute("~*\\.(css|js)$", config)
|
||||
|
||||
if len(router.Routes()) != 1 {
|
||||
t.Errorf("Expected 1 regex route, got %d", len(router.Routes()))
|
||||
}
|
||||
|
||||
if router.Routes()[0].CaseSensitive {
|
||||
t.Error("Expected case-insensitive route")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_InvalidRegexPattern(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"root": "./static"}
|
||||
|
||||
// Invalid regex - unmatched bracket
|
||||
router.AddRoute("~^/api/[invalid", config)
|
||||
|
||||
// Should not add invalid pattern
|
||||
if len(router.Routes()) != 0 {
|
||||
t.Error("Should not add invalid regex pattern")
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Route Matching Tests ==============
|
||||
|
||||
func TestRouter_MatchExactRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"return": "200 OK"}
|
||||
router.AddRoute("=/health", config)
|
||||
|
||||
match := router.Match("/health")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected match for /health")
|
||||
}
|
||||
|
||||
if match.Config["return"] != "200 OK" {
|
||||
t.Error("Expected return config")
|
||||
}
|
||||
|
||||
if len(match.Params) != 0 {
|
||||
t.Error("Expected empty params for exact match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchExactRouteNoMatch(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"return": "200 OK"}
|
||||
router.AddRoute("=/health", config)
|
||||
|
||||
match := router.Match("/healthcheck")
|
||||
|
||||
if match != nil {
|
||||
t.Error("Exact route should not match /healthcheck")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchRegexRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"proxy_pass": "http://localhost:9001"}
|
||||
router.AddRoute("~^/api/v\\d+/", config)
|
||||
|
||||
match := router.Match("/api/v1/users")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected match for /api/v1/users")
|
||||
}
|
||||
|
||||
if match.Config["proxy_pass"] != "http://localhost:9001" {
|
||||
t.Error("Expected proxy_pass config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchRegexRouteWithGroups(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"proxy_pass": "http://localhost:9001"}
|
||||
router.AddRoute("~^/api/v(?P<version>\\d+)/", config)
|
||||
|
||||
match := router.Match("/api/v2/data")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected match for /api/v2/data")
|
||||
}
|
||||
|
||||
if match.Params["version"] != "2" {
|
||||
t.Errorf("Expected version=2, got %s", match.Params["version"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchCaseInsensitiveRegex(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"root": "./static", "cache_control": "public, max-age=3600"}
|
||||
router.AddRoute("~*\\.(CSS|JS)$", config)
|
||||
|
||||
// Should match lowercase
|
||||
match1 := router.Match("/styles/main.css")
|
||||
if match1 == nil {
|
||||
t.Error("Should match lowercase .css")
|
||||
}
|
||||
|
||||
// Should match uppercase
|
||||
match2 := router.Match("/scripts/app.JS")
|
||||
if match2 == nil {
|
||||
t.Error("Should match uppercase .JS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchCaseSensitiveRegex(t *testing.T) {
|
||||
router := NewRouter()
|
||||
config := map[string]interface{}{"root": "./static"}
|
||||
router.AddRoute("~\\.(css)$", config)
|
||||
|
||||
// Should match lowercase
|
||||
match1 := router.Match("/styles/main.css")
|
||||
if match1 == nil {
|
||||
t.Error("Should match lowercase .css")
|
||||
}
|
||||
|
||||
// Should NOT match uppercase
|
||||
match2 := router.Match("/styles/main.CSS")
|
||||
if match2 != nil {
|
||||
t.Error("Should not match uppercase .CSS for case-sensitive regex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_MatchDefaultRoute(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.AddRoute("=/health", map[string]interface{}{"return": "200 OK"})
|
||||
router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true})
|
||||
|
||||
match := router.Match("/unknown/path")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected default route match")
|
||||
}
|
||||
|
||||
if match.Config["spa_fallback"] != true {
|
||||
t.Error("Expected spa_fallback config from default route")
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Priority Tests ==============
|
||||
|
||||
func TestRouter_PriorityExactOverRegex(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.AddRoute("=/api/status", map[string]interface{}{"return": "200 Exact"})
|
||||
router.AddRoute("~^/api/", map[string]interface{}{"proxy_pass": "http://localhost:9001"})
|
||||
|
||||
match := router.Match("/api/status")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected match")
|
||||
}
|
||||
|
||||
if match.Config["return"] != "200 Exact" {
|
||||
t.Error("Exact match should have priority over regex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_PriorityRegexOverDefault(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.AddRoute("~^/api/", map[string]interface{}{"proxy_pass": "http://localhost:9001"})
|
||||
router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true})
|
||||
|
||||
match := router.Match("/api/v1/users")
|
||||
|
||||
if match == nil {
|
||||
t.Fatal("Expected match")
|
||||
}
|
||||
|
||||
if match.Config["proxy_pass"] != "http://localhost:9001" {
|
||||
t.Error("Regex match should have priority over default")
|
||||
}
|
||||
}
|
||||
|
||||
// ============== CreateRouterFromConfig Tests ==============
|
||||
|
||||
func TestCreateRouterFromConfig(t *testing.T) {
|
||||
config := map[string]interface{}{
|
||||
"regex_locations": map[string]interface{}{
|
||||
"=/health": map[string]interface{}{
|
||||
"return": "200 OK",
|
||||
"content_type": "text/plain",
|
||||
},
|
||||
"~^/api/": map[string]interface{}{
|
||||
"proxy_pass": "http://localhost:9001",
|
||||
},
|
||||
"__default__": map[string]interface{}{
|
||||
"spa_fallback": true,
|
||||
"root": "./static",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := CreateRouterFromConfig(config)
|
||||
|
||||
// Check exact route
|
||||
if _, ok := router.ExactRoutes()["/health"]; !ok {
|
||||
t.Error("Expected /health exact route")
|
||||
}
|
||||
|
||||
// Check regex route
|
||||
if len(router.Routes()) != 1 {
|
||||
t.Errorf("Expected 1 regex route, got %d", len(router.Routes()))
|
||||
}
|
||||
|
||||
// Check default route
|
||||
if router.DefaultRoute() == nil {
|
||||
t.Error("Expected default route")
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Static Dir Path Tests ==============
|
||||
|
||||
func TestRouter_StaticDirPath(t *testing.T) {
|
||||
router := NewRouter(WithStaticDir("/var/www/html"))
|
||||
|
||||
expected, _ := filepath.Abs("/var/www/html")
|
||||
actual, _ := filepath.Abs(router.StaticDir())
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf("Expected static dir %s, got %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Concurrent Access Tests ==============
|
||||
|
||||
func TestRouter_ConcurrentAccess(t *testing.T) {
|
||||
router := NewRouter()
|
||||
|
||||
// Add routes concurrently
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(n int) {
|
||||
router.AddRoute("~^/api/v"+string(rune('0'+n))+"/", map[string]interface{}{
|
||||
"proxy_pass": "http://localhost:900" + string(rune('0'+n)),
|
||||
})
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Match routes concurrently
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(n int) {
|
||||
router.Match("/api/v" + string(rune('0'+n)) + "/users")
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Benchmarks ==============
|
||||
|
||||
func BenchmarkRouter_MatchExact(b *testing.B) {
|
||||
router := NewRouter()
|
||||
router.AddRoute("=/health", map[string]interface{}{"return": "200 OK"})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.Match("/health")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_MatchRegex(b *testing.B) {
|
||||
router := NewRouter()
|
||||
router.AddRoute("~^/api/v(?P<version>\\d+)/", map[string]interface{}{"proxy_pass": "http://localhost:9001"})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.Match("/api/v1/users/123")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_MatchWithManyRoutes(b *testing.B) {
|
||||
router := NewRouter()
|
||||
|
||||
// Add many routes
|
||||
for i := 0; i < 50; i++ {
|
||||
router.AddRoute("~^/api/v"+string(rune('0'+i%10))+"/service"+string(rune('0'+i/10))+"/",
|
||||
map[string]interface{}{"proxy_pass": "http://localhost:9001"})
|
||||
}
|
||||
router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.Match("/api/v5/service3/users/123")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user