Quellcode durchsuchen

enhance: streamline sandbox configuration and include handling #1421

0xJacky vor 3 Monaten
Ursprung
Commit
d6cdf476a9
2 geänderte Dateien mit 300 neuen und 368 gelöschten Zeilen
  1. 238 154
      internal/nginx/sandbox.go
  2. 62 214
      internal/nginx/sandbox_test.go

+ 238 - 154
internal/nginx/sandbox.go

@@ -2,6 +2,7 @@ package nginx
 
 import (
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -12,16 +13,6 @@ import (
 	"github.com/uozi-tech/cosy/logger"
 )
 
-// Site represents minimal site info needed for sandbox testing
-type SandboxSite struct {
-	Path string
-}
-
-// Stream represents minimal stream info needed for sandbox testing
-type SandboxStream struct {
-	Path string
-}
-
 // NamespaceInfo represents minimal namespace info for sandbox
 type NamespaceInfo struct {
 	ID         uint64
@@ -44,6 +35,11 @@ func SandboxTestConfigWithPaths(namespace *NamespaceInfo, sitePaths, streamPaths
 		return "Config validation skipped for remote-only namespace", nil
 	}
 
+	// If namespace is nil, directly test in real directory (no sandbox)
+	if namespace == nil {
+		return TestConfig()
+	}
+
 	// Create sandbox and test
 	sandbox, err := createSandbox(namespace, sitePaths, streamPaths)
 	if err != nil {
@@ -81,14 +77,31 @@ func createSandbox(namespace *NamespaceInfo, sitePaths, streamPaths []string) (*
 		Namespace: namespace,
 	}
 
-	// Copy necessary directories to sandbox for complete isolation
-	if err := copySandboxDependencies(tempDir); err != nil {
+	// Copy full nginx conf directory to sandbox, excluding sites-* and streams-*
+	if err := copyConfigBaseExceptSitesStreams(tempDir); err != nil {
+		os.RemoveAll(tempDir)
+		return nil, fmt.Errorf("failed to copy base configs: %w", err)
+	}
+
+	// Ensure sandbox sub-directories exist for selected includes
+	if err := os.MkdirAll(filepath.Join(tempDir, "sites-enabled"), 0755); err != nil {
+		os.RemoveAll(tempDir)
+		return nil, fmt.Errorf("failed to create sandbox sites-enabled: %w", err)
+	}
+	if err := os.MkdirAll(filepath.Join(tempDir, "streams-enabled"), 0755); err != nil {
+		os.RemoveAll(tempDir)
+		return nil, fmt.Errorf("failed to create sandbox streams-enabled: %w", err)
+	}
+
+	// Collect and copy only enabled sites/streams for the given namespace
+	siteFiles, streamFiles, err := collectAndCopyNamespaceEnabled(namespace, sitePaths, streamPaths, tempDir)
+	if err != nil {
 		os.RemoveAll(tempDir)
-		return nil, fmt.Errorf("failed to copy sandbox dependencies: %w", err)
+		return nil, fmt.Errorf("failed to collect/copy namespace configs: %w", err)
 	}
 
 	// Generate sandbox nginx.conf
-	configContent, err := generateSandboxConfig(namespace, sitePaths, streamPaths, tempDir)
+	configContent, err := generateSandboxConfig(namespace, siteFiles, streamFiles, tempDir)
 	if err != nil {
 		os.RemoveAll(tempDir)
 		return nil, fmt.Errorf("failed to generate sandbox config: %w", err)
@@ -105,73 +118,6 @@ func createSandbox(namespace *NamespaceInfo, sitePaths, streamPaths []string) (*
 	return sandbox, nil
 }
 
-// copySandboxDependencies copies necessary config directories to sandbox
-func copySandboxDependencies(sandboxDir string) error {
-	confBase := GetConfPath()
-
-	// Directories to copy for complete isolation
-	dirsToCopy := []string{
-		"conf.d",
-		"modules-enabled",
-		"snippets", // Common nginx snippets directory
-	}
-
-	for _, dir := range dirsToCopy {
-		srcDir := filepath.Join(confBase, dir)
-		dstDir := filepath.Join(sandboxDir, dir)
-
-		// Check if source directory exists
-		if !helper.FileExists(srcDir) {
-			continue // Skip non-existent directories
-		}
-
-		// Create destination directory
-		if err := os.MkdirAll(dstDir, 0755); err != nil {
-			return fmt.Errorf("failed to create %s: %w", dir, err)
-		}
-
-		// Copy all files from source to destination
-		entries, err := os.ReadDir(srcDir)
-		if err != nil {
-			logger.Warnf("Failed to read %s: %v, skipping", srcDir, err)
-			continue
-		}
-
-		for _, entry := range entries {
-			if entry.IsDir() {
-				continue // Skip subdirectories for now
-			}
-
-			srcFile := filepath.Join(srcDir, entry.Name())
-			dstFile := filepath.Join(dstDir, entry.Name())
-
-			content, err := os.ReadFile(srcFile)
-			if err != nil {
-				logger.Warnf("Failed to read %s: %v, skipping", srcFile, err)
-				continue
-			}
-
-			if err := os.WriteFile(dstFile, content, 0644); err != nil {
-				logger.Warnf("Failed to write %s: %v, skipping", dstFile, err)
-				continue
-			}
-		}
-
-		logger.Debugf("Copied %s to sandbox", dir)
-	}
-
-	// Also copy mime.types if exists
-	mimeTypes := filepath.Join(confBase, "mime.types")
-	if helper.FileExists(mimeTypes) {
-		content, err := os.ReadFile(mimeTypes)
-		if err == nil {
-			os.WriteFile(filepath.Join(sandboxDir, "mime.types"), content, 0644)
-		}
-	}
-
-	return nil
-}
-
 // Cleanup removes the sandbox directory
 func (s *Sandbox) Cleanup() {
 	if s.Dir != "" {
@@ -184,7 +130,7 @@ func (s *Sandbox) Cleanup() {
 }
 
 // generateSandboxConfig generates a minimal nginx.conf that only includes configs from specified paths
-func generateSandboxConfig(namespace *NamespaceInfo, sitePaths, streamPaths []string, sandboxDir string) (string, error) {
+func generateSandboxConfig(namespace *NamespaceInfo, siteFiles, streamFiles []string, sandboxDir string) (string, error) {
 	// Read the main nginx.conf to get basic structure
 	mainConfPath := GetConfEntryPath()
 	mainConf, err := os.ReadFile(mainConfPath)
@@ -195,42 +141,24 @@ func generateSandboxConfig(namespace *NamespaceInfo, sitePaths, streamPaths []st
 	mainConfStr := string(mainConf)
 
 	// Generate include patterns based on provided paths
-	var includePatterns []string
-
-	// Add site includes
-	for _, sitePath := range sitePaths {
-		siteEnabledPath := GetConfPath("sites-enabled", filepath.Base(sitePath))
-		if helper.FileExists(siteEnabledPath) {
-			includePatterns = append(includePatterns, fmt.Sprintf("    include %s;", siteEnabledPath))
-		}
+	siteIncludeLines := make([]string, 0, len(siteFiles))
+	for _, f := range siteFiles {
+		siteIncludeLines = append(siteIncludeLines, fmt.Sprintf("    include %s;", filepath.Join(sandboxDir, "sites-enabled", f)))
 	}
-
-	// Add stream includes
-	for _, streamPath := range streamPaths {
-		streamEnabledPath := GetConfPath("streams-enabled", filepath.Base(streamPath))
-		if helper.FileExists(streamEnabledPath) {
-			includePatterns = append(includePatterns, fmt.Sprintf("    include %s;", streamEnabledPath))
-		}
-	}
-
-	// If no paths provided, test all enabled configs (original behavior)
-	if len(includePatterns) == 0 {
-		sitesEnabledDir := GetConfPath("sites-enabled")
-		streamsEnabledDir := GetConfPath("streams-enabled")
-
-		includePatterns = append(includePatterns, fmt.Sprintf("    include %s/*;", sitesEnabledDir))
-		includePatterns = append(includePatterns, fmt.Sprintf("    include %s/*;", streamsEnabledDir))
+	streamIncludeLines := make([]string, 0, len(streamFiles))
+	for _, f := range streamFiles {
+		streamIncludeLines = append(streamIncludeLines, fmt.Sprintf("    include %s;", filepath.Join(sandboxDir, "streams-enabled", f)))
 	}
 
 	// Replace include directives with sandbox-specific ones
-	sandboxConf := replaceIncludeDirectives(mainConfStr, includePatterns, sandboxDir)
+	sandboxConf := replaceIncludeDirectives(mainConfStr, sandboxDir, siteIncludeLines, streamIncludeLines)
 
 	return sandboxConf, nil
 }
 
 // replaceIncludeDirectives replaces only sites-enabled and streams-enabled includes
-// Rewrites other includes (conf.d, mime.types, etc.) to use sandbox paths
-func replaceIncludeDirectives(mainConf string, includePatterns []string, sandboxDir string) string {
+// Rewrites other includes to point to copied files under sandboxDir, preserving isolation.
+func replaceIncludeDirectives(mainConf string, sandboxDir string, siteIncludeLines, streamIncludeLines []string) string {
 	lines := strings.Split(mainConf, "\n")
 	var result []string
 	insideHTTP := false
@@ -263,28 +191,22 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
 				// Add our sandbox-specific includes at the first occurrence
 				if insideHTTP && isSitesEnabled && !httpIncludesAdded {
 					result = append(result, "    # Sandbox-specific includes (generated for isolated testing)")
-					for _, pattern := range includePatterns {
-						if strings.Contains(pattern, "sites-enabled") {
-							result = append(result, pattern)
-						}
-					}
+					result = append(result, siteIncludeLines...)
 					httpIncludesAdded = true
 				}
 				if insideStream && isStreamsEnabled && !streamIncludesAdded {
 					result = append(result, "    # Sandbox-specific includes (generated for isolated testing)")
-					for _, pattern := range includePatterns {
-						if strings.Contains(pattern, "streams-enabled") {
-							result = append(result, pattern)
-						}
-					}
+					result = append(result, streamIncludeLines...)
 					streamIncludesAdded = true
 				}
 				continue // Skip the original include line
 			}
 
-			// Rewrite other includes to use sandbox paths
-			rewrittenLine := rewriteIncludePath(line, sandboxDir)
-			result = append(result, rewrittenLine)
+			// Rewrite includes to sandbox paths
+			normalized := rewriteIncludeLineToSandbox(line, sandboxDir)
+			if normalized != "" {
+				result = append(result, normalized)
+			}
 			continue
 		}
 
@@ -294,11 +216,7 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
 				// Add includes before closing http block if not added yet
 				if !httpIncludesAdded {
 					result = append(result, "    # Sandbox-specific includes (generated for isolated testing)")
-					for _, pattern := range includePatterns {
-						if strings.Contains(pattern, "sites-enabled") {
-							result = append(result, pattern)
-						}
-					}
+					result = append(result, siteIncludeLines...)
 					httpIncludesAdded = true
 				}
 				insideHTTP = false
@@ -307,11 +225,7 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
 				// Add includes before closing stream block if not added yet
 				if !streamIncludesAdded {
 					result = append(result, "    # Sandbox-specific includes (generated for isolated testing)")
-					for _, pattern := range includePatterns {
-						if strings.Contains(pattern, "streams-enabled") {
-							result = append(result, pattern)
-						}
-					}
+					result = append(result, streamIncludeLines...)
 					streamIncludesAdded = true
 				}
 				insideStream = false
@@ -324,37 +238,207 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
 	return strings.Join(result, "\n")
 }
 
-// rewriteIncludePath rewrites include paths to use sandbox directory
-func rewriteIncludePath(line, sandboxDir string) string {
-	// Extract the include path using regex
-	// Match: include /path/to/file; or include /path/*.conf;
-	includeRegex := regexp.MustCompile(`include\s+([^;]+);`)
+// rewriteIncludeLineToSandbox rewrites include lines to point to files/directories inside sandboxDir.
+// If an include path is relative, it will be rewritten relative to the nginx conf dir inside sandbox.
+func rewriteIncludeLineToSandbox(line string, sandboxDir string) string {
+	includeRegex := regexp.MustCompile(`(?i)include\s+([^;#]+);`)
 	matches := includeRegex.FindStringSubmatch(line)
-
 	if len(matches) < 2 {
-		return line // No match, return original
+		return line
 	}
+	path := strings.TrimSpace(matches[1])
 
-	origPath := strings.TrimSpace(matches[1])
 	confBase := GetConfPath()
+	var rewritten string
+	if filepath.IsAbs(path) {
+		// If absolute under confBase, map to sandbox
+		if helper.IsUnderDirectory(path, confBase) {
+			rel, err := filepath.Rel(confBase, path)
+			if err == nil {
+				rewritten = filepath.Join(sandboxDir, rel)
+			}
+		}
+	} else {
+		// Relative includes should point inside sandbox conf root
+		rewritten = filepath.Join(sandboxDir, path)
+	}
+	if rewritten == "" {
+		rewritten = path
+	}
+	trimmed := includeRegex.ReplaceAllString(line, "include "+rewritten+";")
+	return trimmed
+}
+
+// collectAndCopyNamespaceEnabled collects and copies enabled site/stream configs based on provided paths.
+// It rewrites relative includes to absolute, and writes them into sandboxDir/{sites-enabled,streams-enabled}.
+// Returns the written file names.
+func collectAndCopyNamespaceEnabled(_ *NamespaceInfo, sitePaths, streamPaths []string, sandboxDir string) (siteFiles, streamFiles []string, err error) {
+	// Helper to process and write a single config by kind and name
+	readSourceAndWrite := func(kind, name string) (writtenName string, wErr error) {
+		var enabledCandidates []string
+		switch kind {
+		case "site":
+			enabledCandidates = []string{
+				GetConfSymlinkPath(GetConfPath("sites-enabled", name)),
+				GetConfPath("sites-enabled", name),
+			}
+		case "stream":
+			enabledCandidates = []string{
+				GetConfSymlinkPath(GetConfPath("streams-enabled", name)),
+				GetConfPath("streams-enabled", name),
+			}
+		}
+		var enabledPath string
+		for _, cand := range enabledCandidates {
+			if helper.FileExists(cand) {
+				enabledPath = cand
+				break
+			}
+		}
+		if enabledPath == "" {
+			return "", nil // not enabled, skip silently
+		}
+
+		// Determine source file: prefer the symlink target if possible; fallback to *-available
+		srcPath := enabledPath
+		if fi, lErr := os.Lstat(enabledPath); lErr == nil && (fi.Mode()&os.ModeSymlink) != 0 {
+			if target, rErr := os.Readlink(enabledPath); rErr == nil {
+				// If target is relative, resolve against enabled dir
+				if !filepath.IsAbs(target) {
+					target = filepath.Join(filepath.Dir(enabledPath), target)
+				}
+				srcPath = target
+			}
+		}
+		if kind == "site" && !helper.FileExists(srcPath) {
+			srcPath = GetConfPath("sites-available", name)
+		}
+		if kind == "stream" && !helper.FileExists(srcPath) {
+			srcPath = GetConfPath("streams-available", name)
+		}
+		content, rErr := os.ReadFile(srcPath)
+		if rErr != nil {
+			return "", fmt.Errorf("read %s content %s: %w", kind, srcPath, rErr)
+		}
+
+		// Rewrite include lines to sandbox paths (resolve relative to source dir first)
+		absRewriter := regexp.MustCompile(`(?m)^[ \t]*include\s+([^;#]+);`)
+		rewritten := absRewriter.ReplaceAllStringFunc(string(content), func(m string) string {
+			return normalizeIncludeLineRelativeTo(m, filepath.Dir(srcPath), sandboxDir)
+		})
+
+		// Compute destination file name respecting platform symlink naming
+		var destName string
+		switch kind {
+		case "site":
+			destName = filepath.Base(GetConfSymlinkPath(GetConfPath("sites-enabled", name)))
+		case "stream":
+			destName = filepath.Base(GetConfSymlinkPath(GetConfPath("streams-enabled", name)))
+		}
+
+		destDir := filepath.Join(sandboxDir, kind+"s-enabled")
+		if err := os.WriteFile(filepath.Join(destDir, destName), []byte(rewritten), 0644); err != nil {
+			return "", fmt.Errorf("write sandbox %s: %w", kind, err)
+		}
+		return destName, nil
+	}
+
+	// Process sites based on provided sitePaths
+	for _, sp := range sitePaths {
+		name := filepath.Base(sp)
+		if written, wErr := readSourceAndWrite("site", name); wErr != nil {
+			return nil, nil, wErr
+		} else if written != "" {
+			siteFiles = append(siteFiles, written)
+		}
+	}
+
+	// Process streams based on provided streamPaths
+	for _, st := range streamPaths {
+		name := filepath.Base(st)
+		if written, wErr := readSourceAndWrite("stream", name); wErr != nil {
+			return nil, nil, wErr
+		} else if written != "" {
+			streamFiles = append(streamFiles, written)
+		}
+	}
 
-	// Paths to rewrite to sandbox
-	rewritePaths := map[string]string{
-		filepath.Join(confBase, "conf.d"):         filepath.Join(sandboxDir, "conf.d"),
-		filepath.Join(confBase, "modules-enabled"): filepath.Join(sandboxDir, "modules-enabled"),
-		filepath.Join(confBase, "snippets"):        filepath.Join(sandboxDir, "snippets"),
-		filepath.Join(confBase, "mime.types"):      filepath.Join(sandboxDir, "mime.types"),
+	return siteFiles, streamFiles, nil
+}
+
+// normalizeIncludeLineRelativeTo rewrites a single include line:
+// - resolves relative paths against baseDir
+// - if the resolved path is under confBase, map to sandboxDir mirror; else keep as is
+func normalizeIncludeLineRelativeTo(line, baseDir, sandboxDir string) string {
+	includeRegex := regexp.MustCompile(`(?i)include\s+([^;#]+);`)
+	matches := includeRegex.FindStringSubmatch(line)
+	if len(matches) < 2 {
+		return line
+	}
+	path := strings.TrimSpace(matches[1])
+
+	// If relative, make absolute to source file dir
+	resolved := path
+	if !filepath.IsAbs(resolved) {
+		resolved = filepath.Clean(filepath.Join(baseDir, resolved))
+	}
+	confBase := GetConfPath()
+	if helper.IsUnderDirectory(resolved, confBase) {
+		if rel, err := filepath.Rel(confBase, resolved); err == nil {
+			resolved = filepath.Join(sandboxDir, rel)
+		}
 	}
+	return includeRegex.ReplaceAllString(line, "include "+resolved+";")
+}
+
+// copyConfigBaseExceptSitesStreams copies the entire nginx conf directory into sandboxDir,
+// excluding any paths under sites-* and streams-* and skipping the entry nginx.conf (we generate our own).
+func copyConfigBaseExceptSitesStreams(sandboxDir string) error {
+	confBase := GetConfPath()
+	entry := GetConfEntryPath()
 
-	// Check if path starts with any of the rewrite paths
-	newPath := origPath
-	for oldPrefix, newPrefix := range rewritePaths {
-		if strings.HasPrefix(origPath, oldPrefix) {
-			newPath = strings.Replace(origPath, oldPrefix, newPrefix, 1)
-			break
+	copyFile := func(src, dst string, mode fs.FileMode) error {
+		parent := filepath.Dir(dst)
+		if err := os.MkdirAll(parent, 0755); err != nil {
+			return err
 		}
+		data, err := os.ReadFile(src)
+		if err != nil {
+			return err
+		}
+		return os.WriteFile(dst, data, 0644)
 	}
 
-	// Replace in the original line
-	return strings.Replace(line, origPath, newPath, 1)
+	return filepath.WalkDir(confBase, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		rel, rErr := filepath.Rel(confBase, path)
+		if rErr != nil {
+			return rErr
+		}
+		if rel == "." {
+			return nil
+		}
+		// Skip blacklisted directories
+		if d.IsDir() {
+			base := filepath.Base(path)
+			if strings.HasPrefix(base, "sites-") || strings.HasPrefix(base, "streams-") {
+				return filepath.SkipDir
+			}
+			// Create directory in sandbox
+			return os.MkdirAll(filepath.Join(sandboxDir, rel), 0755)
+		}
+		// Skip entry nginx.conf to avoid overwriting generated file
+		if path == entry && filepath.Base(path) == "nginx.conf" {
+			return nil
+		}
+		// Copy regular file (follow symlinks by reading content)
+		dst := filepath.Join(sandboxDir, rel)
+		info, sErr := os.Lstat(path)
+		if sErr != nil {
+			return sErr
+		}
+		return copyFile(path, dst, info.Mode())
+	})
 }

+ 62 - 214
internal/nginx/sandbox_test.go

@@ -1,254 +1,102 @@
 package nginx
 
 import (
-	"os"
+	"path/filepath"
+	"runtime"
 	"strings"
 	"testing"
 )
 
-func TestCreateSandbox(t *testing.T) {
-	namespaceInfo := &NamespaceInfo{
-		ID:   1,
-		Name: "test-namespace",
+func TestNormalizeIncludeLineRelativeTo(t *testing.T) {
+	baseDir := "/etc/nginx/sites-available"
+	if runtime.GOOS == "windows" {
+		// keep test portable; filepath.Join will use OS-specific separator
+		baseDir = `C:\nginx\conf\sites-available`
 	}
+	sandboxDir := "/tmp/sbx"
 
-	sitePaths := []string{"site1.conf", "site2.conf"}
-	streamPaths := []string{"stream1.conf"}
-
-	sandbox, err := createSandbox(namespaceInfo, sitePaths, streamPaths)
-	if err != nil {
-		t.Fatalf("Failed to create sandbox: %v", err)
-	}
-	defer sandbox.Cleanup()
-
-	// Verify sandbox directory exists
-	if _, err := os.Stat(sandbox.Dir); os.IsNotExist(err) {
-		t.Errorf("Sandbox directory does not exist: %s", sandbox.Dir)
-	}
-
-	// Verify config file exists
-	if _, err := os.Stat(sandbox.ConfigPath); os.IsNotExist(err) {
-		t.Errorf("Sandbox config file does not exist: %s", sandbox.ConfigPath)
-	}
-
-	// Verify namespace info
-	if sandbox.Namespace.ID != 1 {
-		t.Errorf("Expected namespace ID 1, got %d", sandbox.Namespace.ID)
-	}
-}
-
-func TestSandboxCleanup(t *testing.T) {
-	sandbox, err := createSandbox(nil, []string{}, []string{})
-	if err != nil {
-		t.Fatalf("Failed to create sandbox: %v", err)
-	}
-
-	sandboxDir := sandbox.Dir
-
-	// Cleanup
-	sandbox.Cleanup()
-
-	// Verify directory is removed
-	if _, err := os.Stat(sandboxDir); !os.IsNotExist(err) {
-		t.Errorf("Sandbox directory still exists after cleanup: %s", sandboxDir)
-	}
-}
-
-func TestGenerateSandboxConfig(t *testing.T) {
-	// Skip this test as it requires mocking GetConfEntryPath
-	// The logic is tested in TestReplaceIncludeDirectives instead
-	t.Skip("Skipping - requires dependency injection refactoring")
-}
-
-func TestReplaceIncludeDirectives(t *testing.T) {
 	tests := []struct {
-		name            string
-		mainConf        string
-		includePatterns []string
-		expectContains  []string
-		expectNotContain []string
+		name string
+		in   string
+		wantPrefix string
 	}{
 		{
-			name: "Replace HTTP includes",
-			mainConf: `http {
-    include /etc/nginx/sites-enabled/*;
-}`,
-			includePatterns: []string{
-				"    include /etc/nginx/sites-enabled/site1.conf;",
-				"    include /etc/nginx/sites-enabled/site2.conf;",
-			},
-			expectContains: []string{
-				"include /etc/nginx/sites-enabled/site1.conf",
-				"include /etc/nginx/sites-enabled/site2.conf",
-				"Sandbox-specific includes",
-			},
-			expectNotContain: []string{
-				"include /etc/nginx/sites-enabled/*",
-			},
+			name: "relative simple file",
+			in:   "    include mime.types;",
+			wantPrefix: "    include ",
 		},
 		{
-			name: "Replace Stream includes",
-			mainConf: `stream {
-    include /etc/nginx/streams-enabled/*;
-}`,
-			includePatterns: []string{
-				"    include /etc/nginx/streams-enabled/stream1.conf;",
-			},
-			expectContains: []string{
-				"include /etc/nginx/streams-enabled/stream1.conf",
-				"Sandbox-specific includes",
-			},
-			expectNotContain: []string{
-				"include /etc/nginx/streams-enabled/*",
-			},
-		},
-		{
-			name: "Rewrite other includes to sandbox",
-			mainConf: `http {
-    include /etc/nginx/mime.types;
-    include /etc/nginx/conf.d/*.conf;
-    include /etc/nginx/sites-enabled/*;
-}`,
-			includePatterns: []string{
-				"    include /etc/nginx/sites-enabled/site1.conf;",
-			},
-			expectContains: []string{
-				"include /tmp/test-sandbox/mime.types", // Rewritten to sandbox
-				"include /tmp/test-sandbox/conf.d/*.conf", // Rewritten to sandbox
-				"include /etc/nginx/sites-enabled/site1.conf",
-			},
-			expectNotContain: []string{
-				"include /etc/nginx/sites-enabled/*",
-				"include /etc/nginx/mime.types", // Should be rewritten
-				"include /etc/nginx/conf.d/*.conf", // Should be rewritten
-			},
+			name: "relative path with subdir",
+			in:   "include ../common/snippets/*.conf;",
+			wantPrefix: "include ",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			sandboxDir := "/tmp/test-sandbox"
-			result := replaceIncludeDirectives(tt.mainConf, tt.includePatterns, sandboxDir)
-
-			for _, expected := range tt.expectContains {
-				if !strings.Contains(result, expected) {
-					t.Errorf("Expected result to contain %q, but it doesn't.\nResult:\n%s", expected, result)
-				}
+			out := normalizeIncludeLineRelativeTo(tt.in, baseDir, sandboxDir)
+			if out == "" {
+				t.Fatalf("expected non-empty include, got empty")
 			}
-
-			for _, notExpected := range tt.expectNotContain {
-				if strings.Contains(result, notExpected) {
-					t.Errorf("Expected result NOT to contain %q, but it does.\nResult:\n%s", notExpected, result)
+			if !strings.HasPrefix(out, tt.wantPrefix) {
+				t.Fatalf("unexpected prefix: %q, got %q", tt.wantPrefix, out)
+			}
+			// if relative input (first two cases), ensure absolute joined path appears
+			if tt.name == "relative simple file" || tt.name == "relative path with subdir" {
+				parts := strings.Split(out, "include ")
+				if len(parts) < 2 {
+					t.Fatalf("malformed include line: %q", out)
+				}
+				pathWithSemi := parts[1]
+				path := strings.TrimSuffix(pathWithSemi, ";")
+				if !filepath.IsAbs(path) {
+					t.Fatalf("expected absolute path, got %q", path)
 				}
 			}
 		})
 	}
 }
 
-func TestReplaceIncludeDirectivesEdgeCases(t *testing.T) {
-	t.Run("Empty include patterns", func(t *testing.T) {
-		mainConf := `http {
-    include /etc/nginx/sites-enabled/*;
-}`
-		result := replaceIncludeDirectives(mainConf, []string{}, "/tmp/test-sandbox")
-
-		// Should still add comment but no includes
-		if !strings.Contains(result, "Sandbox-specific includes") {
-			t.Error("Expected sandbox comment even with empty patterns")
-		}
-	})
-
-	t.Run("No http or stream blocks", func(t *testing.T) {
-		mainConf := `events {
-    worker_connections 1024;
-}`
-		includePatterns := []string{"    include /etc/nginx/sites-enabled/site1.conf;"}
-		result := replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
-
-		// Should preserve original config
-		if !strings.Contains(result, "worker_connections 1024") {
-			t.Error("Original config not preserved when no http/stream blocks")
-		}
-	})
-
-	t.Run("Nested braces", func(t *testing.T) {
-		mainConf := `http {
-    server {
-        location / {
-            return 200;
-        }
-    }
-    include /etc/nginx/sites-enabled/*;
-}`
-		includePatterns := []string{"    include /etc/nginx/sites-enabled/site1.conf;"}
-		result := replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
-
-		// Should preserve nested structure
-		if !strings.Contains(result, "location /") {
-			t.Error("Nested location directive not preserved")
-		}
-
-		// Should replace include
-		if strings.Contains(result, "include /etc/nginx/sites-enabled/*") {
-			t.Error("Generic include should be replaced even with nested braces")
-		}
-	})
-}
-
-func TestSandboxTestConfigWithPaths(t *testing.T) {
-	// Skip this integration test - requires nginx installation and proper setup
-	t.Skip("Skipping integration test - requires nginx binary and proper configuration")
-}
-
-func BenchmarkCreateSandbox(b *testing.B) {
-	namespaceInfo := &NamespaceInfo{
-		ID:   1,
-		Name: "bench-namespace",
-	}
-
-	sitePaths := []string{"site1.conf", "site2.conf", "site3.conf"}
-	streamPaths := []string{"stream1.conf"}
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		sandbox, err := createSandbox(namespaceInfo, sitePaths, streamPaths)
-		if err != nil {
-			b.Fatalf("Failed to create sandbox: %v", err)
-		}
-		sandbox.Cleanup()
-	}
-}
-
-func BenchmarkReplaceIncludeDirectives(b *testing.B) {
+func TestReplaceIncludeDirectives(t *testing.T) {
 	mainConf := `
+user  nginx;
+worker_processes auto;
+error_log  /var/log/nginx/error.log notice;
+pid        /var/run/nginx.pid;
+
 events {
-    worker_connections 1024;
+    worker_connections  1024;
 }
 
 http {
-    include /etc/nginx/mime.types;
-    include /etc/nginx/conf.d/*.conf;
-    include /etc/nginx/sites-enabled/*;
-
-    server {
-        listen 80;
-        server_name default;
-    }
+    include       mime.types;
+    include       /etc/nginx/conf.d/*.conf;
+    include       /etc/nginx/sites-enabled/*;
 }
 
 stream {
     include /etc/nginx/streams-enabled/*;
 }
 `
-	includePatterns := []string{
-		"    include /etc/nginx/sites-enabled/site1.conf;",
-		"    include /etc/nginx/sites-enabled/site2.conf;",
-		"    include /etc/nginx/sites-enabled/site3.conf;",
-		"    include /etc/nginx/streams-enabled/stream1.conf;",
-	}
+	siteLines := []string{"    include /tmp/sbx/sites-enabled/a.conf;"}
+	streamLines := []string{"    include /tmp/sbx/streams-enabled/s1.conf;"}
+
+	out := replaceIncludeDirectives(mainConf, "/tmp/sbx", siteLines, streamLines)
 
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		_ = replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
+	if strings.Contains(out, "/etc/nginx/sites-enabled/*") {
+		t.Fatal("sites-enabled wildcard should be replaced by sandbox files")
+	}
+	if !strings.Contains(out, "/tmp/sbx/sites-enabled/a.conf;") {
+		t.Fatal("sandbox site include missing")
+	}
+	if strings.Contains(out, "/etc/nginx/streams-enabled/*") {
+		t.Fatal("streams-enabled wildcard should be replaced by sandbox files")
+	}
+	if !strings.Contains(out, "/tmp/sbx/streams-enabled/s1.conf;") {
+		t.Fatal("sandbox stream include missing")
+	}
+	// mime.types should be kept (possibly normalized)
+	if !strings.Contains(strings.ToLower(out), "include") {
+		t.Fatal("expected include directives to remain")
 	}
 }