From 0b83afa6a5b5f51977cd9e2dd80d8e1be8257127 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 30 Aug 2025 21:55:54 +0300 Subject: [PATCH] storage tests Signed-off-by: Mohammed Al Sahaf --- storage_test.go | 692 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 storage_test.go diff --git a/storage_test.go b/storage_test.go new file mode 100644 index 000000000..bf0334925 --- /dev/null +++ b/storage_test.go @@ -0,0 +1,692 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/caddyserver/certmagic" +) + +func TestHomeDir_CrossPlatform(t *testing.T) { + // Save original environment + originalEnv := map[string]string{ + "HOME": os.Getenv("HOME"), + "HOMEDRIVE": os.Getenv("HOMEDRIVE"), + "HOMEPATH": os.Getenv("HOMEPATH"), + "USERPROFILE": os.Getenv("USERPROFILE"), + "home": os.Getenv("home"), // Plan9 + } + defer func() { + // Restore environment + for key, value := range originalEnv { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + }() + + tests := []struct { + name string + setup func() + expected string + }{ + { + name: "normal HOME set", + setup: func() { + os.Clearenv() + os.Setenv("HOME", "/home/user") + }, + expected: "/home/user", + }, + { + name: "no environment variables", + setup: func() { + os.Clearenv() + }, + expected: ".", // Fallback to current directory + }, + { + name: "windows style with HOMEDRIVE and HOMEPATH", + setup: func() { + os.Clearenv() + os.Setenv("HOMEDRIVE", "C:") + os.Setenv("HOMEPATH", "\\Users\\user") + }, + expected: func() string { + if runtime.GOOS == "windows" { + return "C:\\Users\\user" + } + return "." // Non-windows systems fall back to current dir + }(), + }, + { + name: "windows style with USERPROFILE", + setup: func() { + os.Clearenv() + os.Setenv("USERPROFILE", "C:\\Users\\user") + }, + expected: func() string { + if runtime.GOOS == "windows" { + return "C:\\Users\\user" + } + return "." + }(), + }, + { + name: "plan9 style", + setup: func() { + os.Clearenv() + os.Setenv("home", "/usr/user") + }, + expected: func() string { + if runtime.GOOS == "plan9" { + return "/usr/user" + } + return "." + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.setup() + result := HomeDir() + + if result != test.expected { + t.Errorf("Expected '%s', got '%s'", test.expected, result) + } + + // HomeDir should never return empty string + if result == "" { + t.Error("HomeDir should never return empty string") + } + }) + } +} + +func TestHomeDirUnsafe_EdgeCases(t *testing.T) { + // Save original environment + originalEnv := map[string]string{ + "HOME": os.Getenv("HOME"), + "HOMEDRIVE": os.Getenv("HOMEDRIVE"), + "HOMEPATH": os.Getenv("HOMEPATH"), + "USERPROFILE": os.Getenv("USERPROFILE"), + "home": os.Getenv("home"), + } + defer func() { + for key, value := range originalEnv { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + }() + + tests := []struct { + name string + setup func() + expected string + }{ + { + name: "no environment variables", + setup: func() { + os.Clearenv() + }, + expected: "", // homeDirUnsafe can return empty + }, + { + name: "windows with incomplete HOMEDRIVE/HOMEPATH", + setup: func() { + os.Clearenv() + os.Setenv("HOMEDRIVE", "C:") + // HOMEPATH missing + }, + expected: func() string { + if runtime.GOOS == "windows" { + return "" + } + return "" + }(), + }, + { + name: "windows with only HOMEPATH", + setup: func() { + os.Clearenv() + os.Setenv("HOMEPATH", "\\Users\\user") + // HOMEDRIVE missing + }, + expected: func() string { + if runtime.GOOS == "windows" { + return "" + } + return "" + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.setup() + result := homeDirUnsafe() + + if result != test.expected { + t.Errorf("Expected '%s', got '%s'", test.expected, result) + } + }) + } +} + +func TestAppConfigDir_XDG_Priority(t *testing.T) { + // Save original environment + originalXDG := os.Getenv("XDG_CONFIG_HOME") + defer func() { + if originalXDG == "" { + os.Unsetenv("XDG_CONFIG_HOME") + } else { + os.Setenv("XDG_CONFIG_HOME", originalXDG) + } + }() + + // Test XDG_CONFIG_HOME takes priority + xdgPath := "/custom/config/path" + os.Setenv("XDG_CONFIG_HOME", xdgPath) + + result := AppConfigDir() + expected := filepath.Join(xdgPath, "caddy") + + if result != expected { + t.Errorf("Expected '%s', got '%s'", expected, result) + } + + // Test fallback when XDG_CONFIG_HOME is empty + os.Unsetenv("XDG_CONFIG_HOME") + + result = AppConfigDir() + // Should not be the XDG path anymore + if result == expected { + t.Error("Should not use XDG path when environment variable is unset") + } + // Should contain "caddy" or "Caddy" + if !strings.Contains(strings.ToLower(result), "caddy") { + t.Errorf("Result should contain 'caddy': %s", result) + } +} + +func TestAppDataDir_XDG_Priority(t *testing.T) { + // Save original environment + originalXDG := os.Getenv("XDG_DATA_HOME") + defer func() { + if originalXDG == "" { + os.Unsetenv("XDG_DATA_HOME") + } else { + os.Setenv("XDG_DATA_HOME", originalXDG) + } + }() + + // Test XDG_DATA_HOME takes priority + xdgPath := "/custom/data/path" + os.Setenv("XDG_DATA_HOME", xdgPath) + + result := AppDataDir() + expected := filepath.Join(xdgPath, "caddy") + + if result != expected { + t.Errorf("Expected '%s', got '%s'", expected, result) + } +} + +func TestAppDataDir_PlatformSpecific(t *testing.T) { + // Save original environment + originalEnv := map[string]string{ + "XDG_DATA_HOME": os.Getenv("XDG_DATA_HOME"), + "AppData": os.Getenv("AppData"), + "HOME": os.Getenv("HOME"), + "home": os.Getenv("home"), + } + defer func() { + for key, value := range originalEnv { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + }() + + // Clear XDG to test platform-specific behavior + os.Unsetenv("XDG_DATA_HOME") + + switch runtime.GOOS { + case "windows": + // Test Windows AppData + os.Clearenv() + os.Setenv("AppData", "C:\\Users\\user\\AppData\\Roaming") + + result := AppDataDir() + expected := "C:\\Users\\user\\AppData\\Roaming\\Caddy" + if result != expected { + t.Errorf("Windows: Expected '%s', got '%s'", expected, result) + } + + case "darwin": + // Test macOS Application Support + os.Clearenv() + os.Setenv("HOME", "/Users/user") + + result := AppDataDir() + expected := "/Users/user/Library/Application Support/Caddy" + if result != expected { + t.Errorf("macOS: Expected '%s', got '%s'", expected, result) + } + + case "plan9": + // Test Plan9 lib directory + os.Clearenv() + os.Setenv("home", "/usr/user") + + result := AppDataDir() + expected := "/usr/user/lib/caddy" + if result != expected { + t.Errorf("Plan9: Expected '%s', got '%s'", expected, result) + } + + default: + // Test Unix-like systems + os.Clearenv() + os.Setenv("HOME", "/home/user") + + result := AppDataDir() + expected := "/home/user/.local/share/caddy" + if result != expected { + t.Errorf("Unix: Expected '%s', got '%s'", expected, result) + } + } +} + +func TestAppDataDir_Fallback(t *testing.T) { + // Save original environment + originalEnv := map[string]string{ + "XDG_DATA_HOME": os.Getenv("XDG_DATA_HOME"), + "AppData": os.Getenv("AppData"), + "HOME": os.Getenv("HOME"), + "home": os.Getenv("home"), + } + defer func() { + for key, value := range originalEnv { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + }() + + // Clear all relevant environment variables + os.Clearenv() + + result := AppDataDir() + expected := "./caddy" + + if result != expected { + t.Errorf("Expected fallback '%s', got '%s'", expected, result) + } +} + +func TestConfigAutosavePath_Consistency(t *testing.T) { + // Test that ConfigAutosavePath uses AppConfigDir + configDir := AppConfigDir() + expected := filepath.Join(configDir, "autosave.json") + + if ConfigAutosavePath != expected { + t.Errorf("ConfigAutosavePath inconsistent with AppConfigDir: expected '%s', got '%s'", + expected, ConfigAutosavePath) + } +} + +func TestDefaultStorage_Configuration(t *testing.T) { + // Test that DefaultStorage is properly configured + if DefaultStorage == nil { + t.Fatal("DefaultStorage should not be nil") + } + + // Should use AppDataDir + expectedPath := AppDataDir() + if DefaultStorage.Path != expectedPath { + t.Errorf("DefaultStorage path: expected '%s', got '%s'", + expectedPath, DefaultStorage.Path) + } +} + +func TestAppDataDir_Android_SpecialCase(t *testing.T) { + if runtime.GOOS != "android" { + t.Skip("Android-specific test") + } + + // Save original environment + originalEnv := map[string]string{ + "XDG_DATA_HOME": os.Getenv("XDG_DATA_HOME"), + "HOME": os.Getenv("HOME"), + } + defer func() { + for key, value := range originalEnv { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + }() + + // Clear XDG to test Android-specific behavior + os.Unsetenv("XDG_DATA_HOME") + os.Setenv("HOME", "/data/data/com.app") + + result := AppDataDir() + expected := "/data/data/com.app/caddy" + + if result != expected { + t.Errorf("Android: Expected '%s', got '%s'", expected, result) + } +} + +func TestHomeDir_Android_SpecialCase(t *testing.T) { + // Save original environment + originalHOME := os.Getenv("HOME") + defer func() { + if originalHOME == "" { + os.Unsetenv("HOME") + } else { + os.Setenv("HOME", originalHOME) + } + }() + + // Test Android fallback when HOME is not set + os.Unsetenv("HOME") + + result := HomeDir() + + if runtime.GOOS == "android" { + if result != "/sdcard" { + t.Errorf("Android with no HOME: Expected '/sdcard', got '%s'", result) + } + } else { + if result != "." { + t.Errorf("Non-Android with no HOME: Expected '.', got '%s'", result) + } + } +} + +func TestAppConfigDir_CaseSensitivity(t *testing.T) { + // Save original environment + originalXDG := os.Getenv("XDG_CONFIG_HOME") + defer func() { + if originalXDG == "" { + os.Unsetenv("XDG_CONFIG_HOME") + } else { + os.Setenv("XDG_CONFIG_HOME", originalXDG) + } + }() + + // Clear XDG to test platform-specific subdirectory naming + os.Unsetenv("XDG_CONFIG_HOME") + + result := AppConfigDir() + + // Check that the subdirectory name follows platform conventions + switch runtime.GOOS { + case "windows", "darwin": + if !strings.HasSuffix(result, "Caddy") { + t.Errorf("Expected result to end with 'Caddy' on %s, got '%s'", runtime.GOOS, result) + } + default: + if !strings.HasSuffix(result, "caddy") { + t.Errorf("Expected result to end with 'caddy' on %s, got '%s'", runtime.GOOS, result) + } + } +} + +func TestAppDataDir_EmptyEnvironment_Fallback(t *testing.T) { + // Save all relevant environment variables + envVars := []string{ + "XDG_DATA_HOME", "AppData", "HOME", "home", + "HOMEDRIVE", "HOMEPATH", "USERPROFILE", + } + originalEnv := make(map[string]string) + for _, env := range envVars { + originalEnv[env] = os.Getenv(env) + } + defer func() { + for env, value := range originalEnv { + if value == "" { + os.Unsetenv(env) + } else { + os.Setenv(env, value) + } + } + }() + + // Clear all environment variables + for _, env := range envVars { + os.Unsetenv(env) + } + + result := AppDataDir() + expected := "./caddy" + + if result != expected { + t.Errorf("Expected fallback '%s', got '%s'", expected, result) + } +} + +func TestStorageConverter_Interface(t *testing.T) { + // Test that the interface is properly defined + var _ StorageConverter = (*mockStorageConverter)(nil) +} + +type mockStorageConverter struct { + storage *mockStorage + err error +} + +func (m *mockStorageConverter) CertMagicStorage() (certmagic.Storage, error) { + if m.err != nil { + return nil, m.err + } + return m.storage, nil +} + +type mockStorage struct { + data map[string][]byte +} + +func (m *mockStorage) Lock(ctx context.Context, key string) error { + return nil +} + +func (m *mockStorage) Unlock(ctx context.Context, key string) error { + return nil +} + +func (m *mockStorage) Store(ctx context.Context, key string, value []byte) error { + if m.data == nil { + m.data = make(map[string][]byte) + } + m.data[key] = value + return nil +} + +func (m *mockStorage) Load(ctx context.Context, key string) ([]byte, error) { + if m.data == nil { + return nil, fmt.Errorf("not found") + } + value, exists := m.data[key] + if !exists { + return nil, fmt.Errorf("not found") + } + return value, nil +} + +func (m *mockStorage) Delete(ctx context.Context, key string) error { + if m.data == nil { + return nil + } + delete(m.data, key) + return nil +} + +func (m *mockStorage) Exists(ctx context.Context, key string) bool { + if m.data == nil { + return false + } + _, exists := m.data[key] + return exists +} + +func (m *mockStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) { + if m.data == nil { + return nil, nil + } + var keys []string + for key := range m.data { + if strings.HasPrefix(key, prefix) { + keys = append(keys, key) + } + } + return keys, nil +} + +func (m *mockStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) { + if !m.Exists(ctx, key) { + return certmagic.KeyInfo{}, fmt.Errorf("not found") + } + value := m.data[key] + return certmagic.KeyInfo{ + Key: key, + Modified: time.Now(), + Size: int64(len(value)), + IsTerminal: true, + }, nil +} + +func TestStorageConverter_Implementation(t *testing.T) { + mockStore := &mockStorage{} + converter := &mockStorageConverter{storage: mockStore} + + storage, err := converter.CertMagicStorage() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if storage != mockStore { + t.Error("Expected same storage instance") + } +} + +func TestStorageConverter_Error(t *testing.T) { + expectedErr := fmt.Errorf("storage error") + converter := &mockStorageConverter{err: expectedErr} + + storage, err := converter.CertMagicStorage() + if err != expectedErr { + t.Errorf("Expected error %v, got %v", expectedErr, err) + } + if storage != nil { + t.Error("Expected nil storage on error") + } +} + +func TestPathConstruction_Consistency(t *testing.T) { + // Test that all path functions return valid, absolute paths + paths := map[string]string{ + "HomeDir": HomeDir(), + "AppConfigDir": AppConfigDir(), + "AppDataDir": AppDataDir(), + "ConfigAutosavePath": ConfigAutosavePath, + } + + for name, path := range paths { + t.Run(name, func(t *testing.T) { + if path == "" { + t.Error("Path should not be empty") + } + + // Path should not contain null bytes or other invalid characters + if strings.Contains(path, "\x00") { + t.Errorf("Path contains null byte: %s", path) + } + + // HomeDir might return "." which is not absolute + if name != "HomeDir" && !filepath.IsAbs(path) { + t.Errorf("Path should be absolute: %s", path) + } + }) + } +} + +func TestDirectory_Creation_Validation(t *testing.T) { + // Test directory paths that might be created + dirs := []string{ + AppConfigDir(), + AppDataDir(), + filepath.Dir(ConfigAutosavePath), + } + + for _, dir := range dirs { + t.Run(dir, func(t *testing.T) { + // Verify the directory path is reasonable + if strings.Contains(dir, "..") { + t.Errorf("Directory path should not contain '..': %s", dir) + } + + // On Unix-like systems, check permissions would be appropriate + if runtime.GOOS != "windows" { + // Directory should be in user space + if strings.HasPrefix(dir, "/etc") || strings.HasPrefix(dir, "/var") { + // These might be valid in some cases, but worth checking + t.Logf("Warning: Directory in system space: %s", dir) + } + } + }) + } +} + +func BenchmarkHomeDir(b *testing.B) { + for i := 0; i < b.N; i++ { + HomeDir() + } +} + +func BenchmarkAppConfigDir(b *testing.B) { + for i := 0; i < b.N; i++ { + AppConfigDir() + } +} + +func BenchmarkAppDataDir(b *testing.B) { + for i := 0; i < b.N; i++ { + AppDataDir() + } +}