Rules-based browser launcher for TUI + GNOME. switchyard.aly.codes
tui gome bowser go
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor configuration and routing into internal packages

Aly Raffauf (Jul 5, 2026, 12:06 PM EDT) 502c3a22 33cdde0b

+1093 -1013
+1 -3
.github/workflows/ci.yml
··· 31 31 git diff --exit-code -- flatpak/go-modules.json 32 32 - name: Test routing package with coverage 33 33 run: just test-routing-coverage 34 - - name: Test command config compatibility 34 + - name: Test config package 35 35 run: just test-config 36 - - name: Test command sanitizer 37 - run: just test-sanitizer 38 36 - name: Display coverage summary 39 37 run: | 40 38 echo "## Test Coverage Summary" >> $GITHUB_STEP_SUMMARY
+13 -68
cmd/switchyard/config.go
··· 6 6 "fmt" 7 7 "os" 8 8 "os/exec" 9 - "path/filepath" 10 9 "strings" 11 10 11 + appconfig "github.com/alyraffauf/switchyard/internal/config" 12 12 "github.com/alyraffauf/switchyard/internal/routing" 13 - "github.com/pelletier/go-toml/v2" 14 13 ) 15 14 16 - type Config = routing.Config 15 + type Config = appconfig.Config 17 16 type Redirection = routing.Redirection 18 17 type Condition = routing.Condition 19 18 type Rule = routing.Rule 20 19 21 - func configDir() string { 22 - if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { 23 - return filepath.Join(xdg, "switchyard") 24 - } 25 - home, _ := os.UserHomeDir() 26 - return filepath.Join(home, ".config", "switchyard") 27 - } 28 - 29 20 func configPath() string { 30 - return filepath.Join(configDir(), "config.toml") 31 - } 32 - 33 - func newDefaultConfig() *Config { 34 - return routing.NewDefaultConfig() 21 + return appconfig.Path() 35 22 } 36 23 37 24 func loadConfig() *Config { 38 - cfg := newDefaultConfig() 39 - 40 - data, err := os.ReadFile(configPath()) 25 + config, err := appconfig.Load(configPath()) 41 26 if err != nil { 42 - return cfg 43 - } 44 - 45 - if err := toml.Unmarshal(data, cfg); err != nil { 46 27 fmt.Fprintf(os.Stderr, "Warning: Failed to parse config file: %v\n", err) 47 28 fmt.Fprintf(os.Stderr, "Using default configuration\n") 48 - return cfg 49 29 } 50 - 51 - var compat struct { 52 - RemoveTrackingParameters *bool `toml:"remove_tracking_parameters"` 53 - SanitizeLinks *bool `toml:"sanitize_links"` 54 - } 55 - if err := toml.Unmarshal(data, &compat); err == nil { 56 - switch { 57 - case compat.RemoveTrackingParameters != nil: 58 - cfg.RemoveTrackingParameters = *compat.RemoveTrackingParameters 59 - case compat.SanitizeLinks != nil: 60 - cfg.RemoveTrackingParameters = *compat.SanitizeLinks 61 - } 62 - } 63 - 64 - return cfg 30 + return config 65 31 } 66 32 67 - func saveConfig(cfg *Config) error { 68 - dir := configDir() 69 - if err := os.MkdirAll(dir, 0755); err != nil { 70 - return err 71 - } 72 - 73 - data, err := toml.Marshal(cfg) 74 - if err != nil { 75 - return err 76 - } 77 - 78 - return os.WriteFile(configPath(), data, 0644) 33 + func saveConfig(config *Config) error { 34 + return appconfig.Save(configPath(), config) 79 35 } 80 36 81 37 // hostCommand runs commands on the host when Switchyard is sandboxed by Flatpak. ··· 106 62 return cmd.Run() 107 63 } 108 64 109 - // save the current cfg to the specified path 110 - func exportConfig(cfg *Config, path string) error { 111 - data, err := toml.Marshal(cfg) 112 - if err != nil { 113 - return err 114 - } 115 - return os.WriteFile(path, data, 0644) 65 + func exportConfig(config *Config, path string) error { 66 + return appconfig.Export(path, config) 116 67 } 117 68 118 - // load cfg from the specified path and replace current config 119 - func importConfig(cfg *Config, path string) error { 120 - data, err := os.ReadFile(path) 69 + func importConfig(config *Config, path string) error { 70 + importedConfig, err := appconfig.Import(path) 121 71 if err != nil { 122 72 return err 123 73 } 124 74 125 - newCfg := newDefaultConfig() 126 - if err := toml.Unmarshal(data, newCfg); err != nil { 127 - return err 128 - } 129 - 130 - *cfg = *newCfg 131 - return saveConfig(cfg) 75 + *config = *importedConfig 76 + return saveConfig(config) 132 77 }
-126
cmd/switchyard/config_compat_test.go
··· 1 - // SPDX-License-Identifier: GPL-3.0-or-later 2 - 3 - package main 4 - 5 - import ( 6 - "os" 7 - "path/filepath" 8 - "testing" 9 - ) 10 - 11 - func assertDefaultConfig(t *testing.T, cfg *Config) { 12 - t.Helper() 13 - 14 - if !cfg.PromptOnClick { 15 - t.Fatal("PromptOnClick default should be true") 16 - } 17 - if !cfg.CheckDefaultBrowser { 18 - t.Fatal("CheckDefaultBrowser default should be true") 19 - } 20 - if !cfg.ForceDarkMode { 21 - t.Fatal("ForceDarkMode default should be true") 22 - } 23 - if !cfg.StayAlive { 24 - t.Fatal("StayAlive default should be true") 25 - } 26 - if cfg.RemoveTrackingParameters { 27 - t.Fatal("RemoveTrackingParameters default should be false") 28 - } 29 - if cfg.Rules == nil { 30 - t.Fatal("Rules default should be an empty slice, not nil") 31 - } 32 - } 33 - 34 - func writeTestConfig(t *testing.T, configData string) string { 35 - t.Helper() 36 - 37 - tempDir := t.TempDir() 38 - t.Setenv("XDG_CONFIG_HOME", tempDir) 39 - 40 - configDir := filepath.Join(tempDir, "switchyard") 41 - if err := os.MkdirAll(configDir, 0755); err != nil { 42 - t.Fatalf("os.MkdirAll() error = %v", err) 43 - } 44 - 45 - configPath := filepath.Join(configDir, "config.toml") 46 - if err := os.WriteFile(configPath, []byte(configData), 0644); err != nil { 47 - t.Fatalf("os.WriteFile() error = %v", err) 48 - } 49 - 50 - return configPath 51 - } 52 - 53 - func TestLoadConfigUsesDefaultsWhenConfigIsMissing(t *testing.T) { 54 - t.Setenv("XDG_CONFIG_HOME", t.TempDir()) 55 - 56 - cfg := loadConfig() 57 - 58 - assertDefaultConfig(t, cfg) 59 - } 60 - 61 - func TestLoadConfigUsesDefaultsWhenConfigIsInvalid(t *testing.T) { 62 - writeTestConfig(t, "prompt_on_click = [invalid\n") 63 - 64 - cfg := loadConfig() 65 - 66 - assertDefaultConfig(t, cfg) 67 - } 68 - 69 - func TestLoadConfigKeepsDefaultsForOmittedFields(t *testing.T) { 70 - writeTestConfig(t, "prompt_on_click = false\n") 71 - 72 - cfg := loadConfig() 73 - 74 - if cfg.PromptOnClick { 75 - t.Fatal("loadConfig() should honor explicit prompt_on_click = false") 76 - } 77 - if !cfg.ForceDarkMode { 78 - t.Fatal("loadConfig() should keep ForceDarkMode default when omitted") 79 - } 80 - if cfg.Rules == nil { 81 - t.Fatal("Rules default should be an empty slice, not nil") 82 - } 83 - } 84 - 85 - func TestImportConfigKeepsDefaultsForOmittedFields(t *testing.T) { 86 - tempDir := t.TempDir() 87 - t.Setenv("XDG_CONFIG_HOME", tempDir) 88 - 89 - importPath := filepath.Join(tempDir, "import.toml") 90 - if err := os.WriteFile(importPath, []byte("prompt_on_click = false\n"), 0644); err != nil { 91 - t.Fatalf("os.WriteFile() error = %v", err) 92 - } 93 - 94 - cfg := &Config{} 95 - if err := importConfig(cfg, importPath); err != nil { 96 - t.Fatalf("importConfig() error = %v", err) 97 - } 98 - 99 - if cfg.PromptOnClick { 100 - t.Fatal("importConfig() should honor explicit prompt_on_click = false") 101 - } 102 - if !cfg.ForceDarkMode { 103 - t.Fatal("importConfig() should keep ForceDarkMode default when omitted") 104 - } 105 - if cfg.Rules == nil { 106 - t.Fatal("Rules default should be an empty slice, not nil") 107 - } 108 - } 109 - 110 - func TestLoadConfigUsesLegacySanitizeLinksKey(t *testing.T) { 111 - writeTestConfig(t, "sanitize_links = true\n") 112 - 113 - cfg := loadConfig() 114 - if !cfg.RemoveTrackingParameters { 115 - t.Fatal("loadConfig() did not honor legacy sanitize_links key") 116 - } 117 - } 118 - 119 - func TestLoadConfigPrefersNewTrackingParameterKey(t *testing.T) { 120 - writeTestConfig(t, "remove_tracking_parameters = false\nsanitize_links = true\n") 121 - 122 - cfg := loadConfig() 123 - if cfg.RemoveTrackingParameters { 124 - t.Fatal("loadConfig() should prefer remove_tracking_parameters over sanitize_links") 125 - } 126 - }
cmd/switchyard/embedded/adguard_filter.txt internal/routing/embedded/adguard_filter.txt
+1 -1
cmd/switchyard/main.go
··· 142 142 } 143 143 144 144 if cfg.RemoveTrackingParameters { 145 - sanitized = removeTrackingParameters(sanitized) 145 + sanitized = routing.RemoveTrackingParameters(sanitized) 146 146 } 147 147 148 148 if len(cfg.Redirections) > 0 {
+24 -28
cmd/switchyard/sanitizer.go internal/routing/sanitizer.go
··· 1 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 2 3 - package main 3 + package routing 4 4 5 5 import ( 6 6 "bufio" ··· 23 23 ) 24 24 25 25 type adGuardRule struct { 26 - isException bool 27 - urlRegex *regexp.Regexp // Filter for the URL (e.g., ||domain.com^) 28 - paramRegex *regexp.Regexp // Filter for the parameter name 29 - paramName string // Literal parameter name if not regex 26 + isException bool 27 + urlRegex *regexp.Regexp 28 + parameterRegex *regexp.Regexp 29 + parameterName string 30 30 } 31 31 32 32 func getTrackingParameterRules() []adGuardRule { ··· 64 64 parts := strings.Split(line, "$") 65 65 urlPattern := parts[0] 66 66 67 - // Convert AdGuard URL pattern to Regex 68 67 if urlPattern != "" { 69 68 pattern := regexp.QuoteMeta(urlPattern) 70 - pattern = strings.ReplaceAll(pattern, `\|\|`, `(^|\.)`) // AdGuard || 71 - pattern = strings.ReplaceAll(pattern, `\^`, `([^a-zA-Z0-9.\-%]|$)`) // AdGuard ^ 69 + pattern = strings.ReplaceAll(pattern, `\|\|`, `(^|\.)`) 70 + pattern = strings.ReplaceAll(pattern, `\^`, `([^a-zA-Z0-9.\-%]|$)`) 72 71 pattern = strings.ReplaceAll(pattern, `\*`, `.*`) 73 72 if re, err := regexp.Compile("(?i)" + pattern); err == nil { 74 73 rule.urlRegex = re 75 74 } 76 75 } 77 76 78 - // Parse $removeparam 79 77 if len(parts) > 1 { 80 78 options := strings.Split(parts[1], ",") 81 79 for _, opt := range options { 82 80 if strings.HasPrefix(opt, "removeparam=") { 83 - p := strings.TrimPrefix(opt, "removeparam=") 84 - if strings.HasPrefix(p, "/") && strings.HasSuffix(p, "/") { 85 - if re, err := regexp.Compile("(?i)" + p[1:len(p)-1]); err == nil { 86 - rule.paramRegex = re 81 + paramValue := strings.TrimPrefix(opt, "removeparam=") 82 + if strings.HasPrefix(paramValue, "/") && strings.HasSuffix(paramValue, "/") { 83 + if re, err := regexp.Compile("(?i)" + paramValue[1:len(paramValue)-1]); err == nil { 84 + rule.parameterRegex = re 87 85 } 88 86 } else { 89 - rule.paramName = p 87 + rule.parameterName = paramValue 90 88 } 91 89 return rule, true 92 90 } else if opt == "removeparam" { 93 - rule.paramName = "*" // Remove all tracking params matches 91 + rule.parameterName = "*" 94 92 return rule, true 95 93 } 96 94 } ··· 98 96 return adGuardRule{}, false 99 97 } 100 98 101 - func removeTrackingParameters(rawURL string) string { 99 + func RemoveTrackingParameters(rawURL string) string { 102 100 u, err := url.Parse(rawURL) 103 101 if err != nil || u.RawQuery == "" { 104 102 return rawURL ··· 108 106 } 109 107 110 108 func removeTrackingParametersWithRules(rawURL string, u *url.URL, rules []adGuardRule) string { 111 - q := u.Query() 109 + query := u.Query() 112 110 changed := false 113 111 114 - for param := range q { 112 + for parameter := range query { 115 113 shouldRemove := false 116 114 isWhitelisted := false 117 115 118 116 for _, rule := range rules { 119 - // 1. Does the URL match this rule's domain/path filter? 120 117 if rule.urlRegex != nil && !rule.urlRegex.MatchString(rawURL) { 121 118 continue 122 119 } 123 120 124 - // 2. Does this parameter match the rule's target? 125 - matchesParam := false 126 - if rule.paramName == "*" || rule.paramName == param { 127 - matchesParam = true 128 - } else if rule.paramRegex != nil && rule.paramRegex.MatchString(param) { 129 - matchesParam = true 121 + matchesParameter := false 122 + if rule.parameterName == "*" || rule.parameterName == parameter { 123 + matchesParameter = true 124 + } else if rule.parameterRegex != nil && rule.parameterRegex.MatchString(parameter) { 125 + matchesParameter = true 130 126 } 131 127 132 - if matchesParam { 128 + if matchesParameter { 133 129 if rule.isException { 134 130 isWhitelisted = true 135 131 break ··· 139 135 } 140 136 141 137 if shouldRemove && !isWhitelisted { 142 - q.Del(param) 138 + query.Del(parameter) 143 139 changed = true 144 140 } 145 141 } ··· 148 144 return rawURL 149 145 } 150 146 151 - u.RawQuery = q.Encode() 147 + u.RawQuery = query.Encode() 152 148 return u.String() 153 149 }
+9 -9
cmd/switchyard/sanitizer_test.go internal/routing/sanitizer_test.go
··· 1 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 2 3 - package main 3 + package routing 4 4 5 5 import ( 6 6 "net/url" ··· 36 36 }, 37 37 } 38 38 39 - for _, tt := range tests { 40 - t.Run(tt.name, func(t *testing.T) { 41 - parsed, err := url.Parse(tt.url) 39 + for _, test := range tests { 40 + t.Run(test.name, func(t *testing.T) { 41 + parsed, err := url.Parse(test.url) 42 42 if err != nil { 43 43 t.Fatalf("url.Parse() error = %v", err) 44 44 } 45 45 46 - got := removeTrackingParametersWithRules(tt.url, parsed, rules) 47 - if got != tt.want { 48 - t.Fatalf("removeTrackingParametersWithRules() = %q, want %q", got, tt.want) 46 + got := removeTrackingParametersWithRules(test.url, parsed, rules) 47 + if got != test.want { 48 + t.Fatalf("removeTrackingParametersWithRules() = %q, want %q", got, test.want) 49 49 } 50 50 }) 51 51 } 52 52 } 53 53 54 54 func TestRemoveTrackingParametersUsesBundledRules(t *testing.T) { 55 - got := removeTrackingParameters("https://example.com/article?id=1&utm_source=newsletter") 55 + got := RemoveTrackingParameters("https://example.com/article?id=1&utm_source=newsletter") 56 56 want := "https://example.com/article?id=1" 57 57 58 58 if got != want { 59 - t.Fatalf("removeTrackingParameters() = %q, want %q", got, want) 59 + t.Fatalf("RemoveTrackingParameters() = %q, want %q", got, want) 60 60 } 61 61 }
+130
internal/config/config.go
··· 1 + // SPDX-License-Identifier: GPL-3.0-or-later 2 + 3 + package config 4 + 5 + import ( 6 + "os" 7 + "path/filepath" 8 + 9 + "github.com/alyraffauf/switchyard/internal/routing" 10 + "github.com/pelletier/go-toml/v2" 11 + ) 12 + 13 + type Config struct { 14 + PromptOnClick bool `toml:"prompt_on_click"` 15 + FavoriteBrowser string `toml:"favorite_browser"` 16 + HiddenBrowsers []string `toml:"hidden_browsers"` 17 + CheckDefaultBrowser bool `toml:"check_default_browser"` 18 + ShowAppNames bool `toml:"show_app_names"` 19 + ForceDarkMode bool `toml:"force_dark_mode"` 20 + StayAlive bool `toml:"stay_alive"` 21 + RemoveTrackingParameters bool `toml:"remove_tracking_parameters"` 22 + Redirections []routing.Redirection `toml:"redirections,omitempty"` 23 + Rules []routing.Rule `toml:"rules"` 24 + } 25 + 26 + func Dir() string { 27 + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { 28 + return filepath.Join(xdgConfigHome, "switchyard") 29 + } 30 + homeDir, _ := os.UserHomeDir() 31 + return filepath.Join(homeDir, ".config", "switchyard") 32 + } 33 + 34 + func Path() string { 35 + return filepath.Join(Dir(), "config.toml") 36 + } 37 + 38 + func NewDefault() *Config { 39 + return &Config{ 40 + PromptOnClick: true, 41 + CheckDefaultBrowser: true, 42 + ShowAppNames: false, 43 + ForceDarkMode: true, 44 + StayAlive: true, 45 + RemoveTrackingParameters: false, 46 + Rules: []routing.Rule{}, 47 + } 48 + } 49 + 50 + func Load(path string) (*Config, error) { 51 + settings := NewDefault() 52 + 53 + data, err := os.ReadFile(path) 54 + if err != nil { 55 + if os.IsNotExist(err) { 56 + return settings, nil 57 + } 58 + return settings, err 59 + } 60 + 61 + if err := toml.Unmarshal(data, settings); err != nil { 62 + return settings, err 63 + } 64 + 65 + applyTrackingParameterCompatibility(settings, data) 66 + return settings, nil 67 + } 68 + 69 + func Save(path string, settings *Config) error { 70 + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 71 + return err 72 + } 73 + 74 + data, err := toml.Marshal(settings) 75 + if err != nil { 76 + return err 77 + } 78 + 79 + return os.WriteFile(path, data, 0644) 80 + } 81 + 82 + func Export(path string, settings *Config) error { 83 + data, err := toml.Marshal(settings) 84 + if err != nil { 85 + return err 86 + } 87 + return os.WriteFile(path, data, 0644) 88 + } 89 + 90 + func Import(path string) (*Config, error) { 91 + settings := NewDefault() 92 + 93 + data, err := os.ReadFile(path) 94 + if err != nil { 95 + return nil, err 96 + } 97 + 98 + if err := toml.Unmarshal(data, settings); err != nil { 99 + return nil, err 100 + } 101 + 102 + applyTrackingParameterCompatibility(settings, data) 103 + return settings, nil 104 + } 105 + 106 + func (settings *Config) MatchRule(url string) (browserID string, alwaysAsk bool, matched bool) { 107 + for _, rule := range settings.Rules { 108 + if rule.MatchesConditions(url) { 109 + return rule.Browser, rule.AlwaysAsk, true 110 + } 111 + } 112 + return "", false, false 113 + } 114 + 115 + func applyTrackingParameterCompatibility(settings *Config, data []byte) { 116 + var compatibility struct { 117 + RemoveTrackingParameters *bool `toml:"remove_tracking_parameters"` 118 + SanitizeLinks *bool `toml:"sanitize_links"` 119 + } 120 + if err := toml.Unmarshal(data, &compatibility); err != nil { 121 + return 122 + } 123 + 124 + switch { 125 + case compatibility.RemoveTrackingParameters != nil: 126 + settings.RemoveTrackingParameters = *compatibility.RemoveTrackingParameters 127 + case compatibility.SanitizeLinks != nil: 128 + settings.RemoveTrackingParameters = *compatibility.SanitizeLinks 129 + } 130 + }
+165
internal/config/config_test.go
··· 1 + // SPDX-License-Identifier: GPL-3.0-or-later 2 + 3 + package config 4 + 5 + import ( 6 + "os" 7 + "path/filepath" 8 + "testing" 9 + ) 10 + 11 + func assertDefaultConfig(t *testing.T, config *Config) { 12 + t.Helper() 13 + 14 + if !config.PromptOnClick { 15 + t.Fatal("PromptOnClick default should be true") 16 + } 17 + if !config.CheckDefaultBrowser { 18 + t.Fatal("CheckDefaultBrowser default should be true") 19 + } 20 + if !config.ForceDarkMode { 21 + t.Fatal("ForceDarkMode default should be true") 22 + } 23 + if !config.StayAlive { 24 + t.Fatal("StayAlive default should be true") 25 + } 26 + if config.RemoveTrackingParameters { 27 + t.Fatal("RemoveTrackingParameters default should be false") 28 + } 29 + if config.Rules == nil { 30 + t.Fatal("Rules default should be an empty slice, not nil") 31 + } 32 + } 33 + 34 + func writeTestConfig(t *testing.T, configData string) string { 35 + t.Helper() 36 + 37 + tempDir := t.TempDir() 38 + t.Setenv("XDG_CONFIG_HOME", tempDir) 39 + 40 + configDir := filepath.Dir(Path()) 41 + if err := os.MkdirAll(configDir, 0755); err != nil { 42 + t.Fatalf("os.MkdirAll() error = %v", err) 43 + } 44 + 45 + configPath := Path() 46 + if err := os.WriteFile(configPath, []byte(configData), 0644); err != nil { 47 + t.Fatalf("os.WriteFile() error = %v", err) 48 + } 49 + 50 + return configPath 51 + } 52 + 53 + func TestLoadUsesDefaultsWhenConfigIsMissing(t *testing.T) { 54 + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) 55 + 56 + config, err := Load(Path()) 57 + if err != nil { 58 + t.Fatalf("Load() error = %v", err) 59 + } 60 + 61 + assertDefaultConfig(t, config) 62 + } 63 + 64 + func TestLoadUsesDefaultsWhenConfigIsInvalid(t *testing.T) { 65 + configPath := writeTestConfig(t, "prompt_on_click = [invalid\n") 66 + 67 + config, err := Load(configPath) 68 + if err == nil { 69 + t.Fatal("Load() expected invalid TOML error") 70 + } 71 + 72 + assertDefaultConfig(t, config) 73 + } 74 + 75 + func TestLoadReturnsReadError(t *testing.T) { 76 + configPath := t.TempDir() 77 + 78 + config, err := Load(configPath) 79 + if err == nil { 80 + t.Fatal("Load() expected read error") 81 + } 82 + 83 + assertDefaultConfig(t, config) 84 + } 85 + 86 + func TestLoadKeepsDefaultsForOmittedFields(t *testing.T) { 87 + configPath := writeTestConfig(t, "prompt_on_click = false\n") 88 + 89 + config, err := Load(configPath) 90 + if err != nil { 91 + t.Fatalf("Load() error = %v", err) 92 + } 93 + 94 + if config.PromptOnClick { 95 + t.Fatal("Load() should honor explicit prompt_on_click = false") 96 + } 97 + if !config.ForceDarkMode { 98 + t.Fatal("Load() should keep ForceDarkMode default when omitted") 99 + } 100 + if config.Rules == nil { 101 + t.Fatal("Rules default should be an empty slice, not nil") 102 + } 103 + } 104 + 105 + func TestImportKeepsDefaultsForOmittedFields(t *testing.T) { 106 + tempDir := t.TempDir() 107 + importPath := filepath.Join(tempDir, "import.toml") 108 + if err := os.WriteFile(importPath, []byte("prompt_on_click = false\n"), 0644); err != nil { 109 + t.Fatalf("os.WriteFile() error = %v", err) 110 + } 111 + 112 + config, err := Import(importPath) 113 + if err != nil { 114 + t.Fatalf("Import() error = %v", err) 115 + } 116 + 117 + if config.PromptOnClick { 118 + t.Fatal("Import() should honor explicit prompt_on_click = false") 119 + } 120 + if !config.ForceDarkMode { 121 + t.Fatal("Import() should keep ForceDarkMode default when omitted") 122 + } 123 + if config.Rules == nil { 124 + t.Fatal("Rules default should be an empty slice, not nil") 125 + } 126 + } 127 + 128 + func TestLoadUsesLegacySanitizeLinksKey(t *testing.T) { 129 + configPath := writeTestConfig(t, "sanitize_links = true\n") 130 + 131 + config, err := Load(configPath) 132 + if err != nil { 133 + t.Fatalf("Load() error = %v", err) 134 + } 135 + if !config.RemoveTrackingParameters { 136 + t.Fatal("Load() did not honor legacy sanitize_links key") 137 + } 138 + } 139 + 140 + func TestLoadPrefersNewTrackingParameterKey(t *testing.T) { 141 + configPath := writeTestConfig(t, "remove_tracking_parameters = false\nsanitize_links = true\n") 142 + 143 + config, err := Load(configPath) 144 + if err != nil { 145 + t.Fatalf("Load() error = %v", err) 146 + } 147 + if config.RemoveTrackingParameters { 148 + t.Fatal("Load() should prefer remove_tracking_parameters over sanitize_links") 149 + } 150 + } 151 + 152 + func TestImportUsesLegacySanitizeLinksKey(t *testing.T) { 153 + importPath := filepath.Join(t.TempDir(), "import.toml") 154 + if err := os.WriteFile(importPath, []byte("sanitize_links = true\n"), 0644); err != nil { 155 + t.Fatalf("os.WriteFile() error = %v", err) 156 + } 157 + 158 + config, err := Import(importPath) 159 + if err != nil { 160 + t.Fatalf("Import() error = %v", err) 161 + } 162 + if !config.RemoveTrackingParameters { 163 + t.Fatal("Import() did not honor legacy sanitize_links key") 164 + } 165 + }
+423
internal/config/match_test.go
··· 1 + // SPDX-License-Identifier: GPL-3.0-or-later 2 + 3 + package config 4 + 5 + import ( 6 + "testing" 7 + 8 + "github.com/alyraffauf/switchyard/internal/routing" 9 + ) 10 + 11 + func TestConfigMatchRule(t *testing.T) { 12 + tests := []struct { 13 + name string 14 + config Config 15 + url string 16 + wantBrowserID string 17 + wantAlwaysAsk bool 18 + wantMatched bool 19 + }{ 20 + { 21 + name: "matches first rule", 22 + config: Config{ 23 + Rules: []routing.Rule{ 24 + { 25 + Name: "GitHub", 26 + Browser: "firefox.desktop", 27 + AlwaysAsk: false, 28 + Conditions: []routing.Condition{ 29 + {Type: "domain", Pattern: "github.com"}, 30 + }, 31 + }, 32 + { 33 + Name: "GitLab", 34 + Browser: "chrome.desktop", 35 + AlwaysAsk: false, 36 + Conditions: []routing.Condition{ 37 + {Type: "domain", Pattern: "gitlab.com"}, 38 + }, 39 + }, 40 + }, 41 + }, 42 + url: "https://github.com/user/repo", 43 + wantBrowserID: "firefox.desktop", 44 + wantAlwaysAsk: false, 45 + wantMatched: true, 46 + }, 47 + { 48 + name: "matches second rule", 49 + config: Config{ 50 + Rules: []routing.Rule{ 51 + { 52 + Name: "GitHub", 53 + Browser: "firefox.desktop", 54 + AlwaysAsk: false, 55 + Conditions: []routing.Condition{ 56 + {Type: "domain", Pattern: "github.com"}, 57 + }, 58 + }, 59 + { 60 + Name: "GitLab", 61 + Browser: "chrome.desktop", 62 + AlwaysAsk: false, 63 + Conditions: []routing.Condition{ 64 + {Type: "domain", Pattern: "gitlab.com"}, 65 + }, 66 + }, 67 + }, 68 + }, 69 + url: "https://gitlab.com/user/repo", 70 + wantBrowserID: "chrome.desktop", 71 + wantAlwaysAsk: false, 72 + wantMatched: true, 73 + }, 74 + { 75 + name: "no rules match", 76 + config: Config{ 77 + Rules: []routing.Rule{ 78 + { 79 + Name: "GitHub", 80 + Browser: "firefox.desktop", 81 + AlwaysAsk: false, 82 + Conditions: []routing.Condition{ 83 + {Type: "domain", Pattern: "github.com"}, 84 + }, 85 + }, 86 + }, 87 + }, 88 + url: "https://example.com", 89 + wantBrowserID: "", 90 + wantAlwaysAsk: false, 91 + wantMatched: false, 92 + }, 93 + { 94 + name: "rule with always_ask set", 95 + config: Config{ 96 + Rules: []routing.Rule{ 97 + { 98 + Name: "Work Sites", 99 + Browser: "", 100 + AlwaysAsk: true, 101 + Conditions: []routing.Condition{ 102 + {Type: "keyword", Pattern: "work"}, 103 + }, 104 + }, 105 + }, 106 + }, 107 + url: "https://work.example.com", 108 + wantBrowserID: "", 109 + wantAlwaysAsk: true, 110 + wantMatched: true, 111 + }, 112 + { 113 + name: "empty rules list", 114 + config: Config{ 115 + Rules: []routing.Rule{}, 116 + }, 117 + url: "https://example.com", 118 + wantBrowserID: "", 119 + wantAlwaysAsk: false, 120 + wantMatched: false, 121 + }, 122 + { 123 + name: "first matching rule wins", 124 + config: Config{ 125 + Rules: []routing.Rule{ 126 + { 127 + Name: "First Match", 128 + Browser: "first.desktop", 129 + AlwaysAsk: false, 130 + Conditions: []routing.Condition{ 131 + {Type: "keyword", Pattern: "github"}, 132 + }, 133 + }, 134 + { 135 + Name: "Second Match", 136 + Browser: "second.desktop", 137 + AlwaysAsk: false, 138 + Conditions: []routing.Condition{ 139 + {Type: "domain", Pattern: "github.com"}, 140 + }, 141 + }, 142 + }, 143 + }, 144 + url: "https://github.com", 145 + wantBrowserID: "first.desktop", 146 + wantAlwaysAsk: false, 147 + wantMatched: true, 148 + }, 149 + { 150 + name: "complex rule with AND logic matches", 151 + config: Config{ 152 + Rules: []routing.Rule{ 153 + { 154 + Name: "GitHub API Docs", 155 + Browser: "work-browser.desktop", 156 + AlwaysAsk: false, 157 + Logic: "all", 158 + Conditions: []routing.Condition{ 159 + {Type: "domain", Pattern: "docs.github.com"}, 160 + {Type: "keyword", Pattern: "api"}, 161 + }, 162 + }, 163 + }, 164 + }, 165 + url: "https://docs.github.com/api/reference", 166 + wantBrowserID: "work-browser.desktop", 167 + wantAlwaysAsk: false, 168 + wantMatched: true, 169 + }, 170 + { 171 + name: "complex rule with OR logic matches", 172 + config: Config{ 173 + Rules: []routing.Rule{ 174 + { 175 + Name: "Git Platforms", 176 + Browser: "dev-browser.desktop", 177 + AlwaysAsk: false, 178 + Logic: "any", 179 + Conditions: []routing.Condition{ 180 + {Type: "domain", Pattern: "github.com"}, 181 + {Type: "domain", Pattern: "gitlab.com"}, 182 + {Type: "domain", Pattern: "bitbucket.org"}, 183 + }, 184 + }, 185 + }, 186 + }, 187 + url: "https://bitbucket.org/user/repo", 188 + wantBrowserID: "dev-browser.desktop", 189 + wantAlwaysAsk: false, 190 + wantMatched: true, 191 + }, 192 + } 193 + 194 + for _, test := range tests { 195 + t.Run(test.name, func(t *testing.T) { 196 + browserID, alwaysAsk, matched := test.config.MatchRule(test.url) 197 + if browserID != test.wantBrowserID { 198 + t.Errorf("Config.MatchRule(%q) browserID = %q, want %q", test.url, browserID, test.wantBrowserID) 199 + } 200 + if alwaysAsk != test.wantAlwaysAsk { 201 + t.Errorf("Config.MatchRule(%q) alwaysAsk = %v, want %v", test.url, alwaysAsk, test.wantAlwaysAsk) 202 + } 203 + if matched != test.wantMatched { 204 + t.Errorf("Config.MatchRule(%q) matched = %v, want %v", test.url, matched, test.wantMatched) 205 + } 206 + }) 207 + } 208 + } 209 + 210 + func TestConfigMatchRuleOrdering(t *testing.T) { 211 + tests := []struct { 212 + name string 213 + config Config 214 + url string 215 + wantBrowserID string 216 + wantMatched bool 217 + }{ 218 + { 219 + name: "first matching rule wins", 220 + config: Config{ 221 + Rules: []routing.Rule{ 222 + { 223 + Name: "First Rule", 224 + Browser: "first-browser.desktop", 225 + Logic: "all", 226 + Conditions: []routing.Condition{ 227 + {Type: "domain", Pattern: "example.com"}, 228 + }, 229 + }, 230 + { 231 + Name: "Second Rule", 232 + Browser: "second-browser.desktop", 233 + Logic: "all", 234 + Conditions: []routing.Condition{ 235 + {Type: "domain", Pattern: "example.com"}, 236 + }, 237 + }, 238 + }, 239 + }, 240 + url: "https://example.com/path", 241 + wantBrowserID: "first-browser.desktop", 242 + wantMatched: true, 243 + }, 244 + { 245 + name: "more specific rule first", 246 + config: Config{ 247 + Rules: []routing.Rule{ 248 + { 249 + Name: "Specific Rule", 250 + Browser: "specific-browser.desktop", 251 + Logic: "all", 252 + Conditions: []routing.Condition{ 253 + {Type: "domain", Pattern: "docs.example.com"}, 254 + }, 255 + }, 256 + { 257 + Name: "General Rule", 258 + Browser: "general-browser.desktop", 259 + Logic: "all", 260 + Conditions: []routing.Condition{ 261 + {Type: "glob", Pattern: "*.example.com"}, 262 + }, 263 + }, 264 + }, 265 + }, 266 + url: "https://docs.example.com/guide", 267 + wantBrowserID: "specific-browser.desktop", 268 + wantMatched: true, 269 + }, 270 + { 271 + name: "general rule matches when specific doesn't", 272 + config: Config{ 273 + Rules: []routing.Rule{ 274 + { 275 + Name: "Specific Rule", 276 + Browser: "specific-browser.desktop", 277 + Logic: "all", 278 + Conditions: []routing.Condition{ 279 + {Type: "domain", Pattern: "docs.example.com"}, 280 + }, 281 + }, 282 + { 283 + Name: "General Rule", 284 + Browser: "general-browser.desktop", 285 + Logic: "all", 286 + Conditions: []routing.Condition{ 287 + {Type: "glob", Pattern: "*.example.com"}, 288 + }, 289 + }, 290 + }, 291 + }, 292 + url: "https://api.example.com/endpoint", 293 + wantBrowserID: "general-browser.desktop", 294 + wantMatched: true, 295 + }, 296 + { 297 + name: "non-matching rules are skipped", 298 + config: Config{ 299 + Rules: []routing.Rule{ 300 + { 301 + Name: "Non-matching Rule", 302 + Browser: "wrong-browser.desktop", 303 + Logic: "all", 304 + Conditions: []routing.Condition{ 305 + {Type: "domain", Pattern: "other.com"}, 306 + }, 307 + }, 308 + { 309 + Name: "Matching Rule", 310 + Browser: "correct-browser.desktop", 311 + Logic: "all", 312 + Conditions: []routing.Condition{ 313 + {Type: "domain", Pattern: "example.com"}, 314 + }, 315 + }, 316 + }, 317 + }, 318 + url: "https://example.com/path", 319 + wantBrowserID: "correct-browser.desktop", 320 + wantMatched: true, 321 + }, 322 + { 323 + name: "empty rules list", 324 + config: Config{ 325 + Rules: []routing.Rule{}, 326 + }, 327 + url: "https://example.com/path", 328 + wantBrowserID: "", 329 + wantMatched: false, 330 + }, 331 + { 332 + name: "all rules fail to match", 333 + config: Config{ 334 + Rules: []routing.Rule{ 335 + { 336 + Name: "Rule 1", 337 + Browser: "browser1.desktop", 338 + Logic: "all", 339 + Conditions: []routing.Condition{ 340 + {Type: "domain", Pattern: "other1.com"}, 341 + }, 342 + }, 343 + { 344 + Name: "Rule 2", 345 + Browser: "browser2.desktop", 346 + Logic: "all", 347 + Conditions: []routing.Condition{ 348 + {Type: "domain", Pattern: "other2.com"}, 349 + }, 350 + }, 351 + }, 352 + }, 353 + url: "https://example.com/path", 354 + wantBrowserID: "", 355 + wantMatched: false, 356 + }, 357 + { 358 + name: "rule with empty conditions is skipped", 359 + config: Config{ 360 + Rules: []routing.Rule{ 361 + { 362 + Name: "Empty Rule", 363 + Browser: "empty-browser.desktop", 364 + Logic: "all", 365 + Conditions: []routing.Condition{}, 366 + }, 367 + { 368 + Name: "Valid Rule", 369 + Browser: "valid-browser.desktop", 370 + Logic: "all", 371 + Conditions: []routing.Condition{ 372 + {Type: "domain", Pattern: "example.com"}, 373 + }, 374 + }, 375 + }, 376 + }, 377 + url: "https://example.com/path", 378 + wantBrowserID: "valid-browser.desktop", 379 + wantMatched: true, 380 + }, 381 + { 382 + name: "AND rule fails partial match, next rule matches", 383 + config: Config{ 384 + Rules: []routing.Rule{ 385 + { 386 + Name: "AND Rule", 387 + Browser: "and-browser.desktop", 388 + Logic: "all", 389 + Conditions: []routing.Condition{ 390 + {Type: "domain", Pattern: "example.com"}, 391 + {Type: "keyword", Pattern: "admin"}, 392 + }, 393 + }, 394 + { 395 + Name: "Fallback Rule", 396 + Browser: "fallback-browser.desktop", 397 + Logic: "all", 398 + Conditions: []routing.Condition{ 399 + {Type: "domain", Pattern: "example.com"}, 400 + }, 401 + }, 402 + }, 403 + }, 404 + url: "https://example.com/user", 405 + wantBrowserID: "fallback-browser.desktop", 406 + wantMatched: true, 407 + }, 408 + } 409 + 410 + for _, test := range tests { 411 + t.Run(test.name, func(t *testing.T) { 412 + browserID, _, matched := test.config.MatchRule(test.url) 413 + if browserID != test.wantBrowserID { 414 + t.Errorf("Config.MatchRule(%q) browserID = %q, want %q", 415 + test.url, browserID, test.wantBrowserID) 416 + } 417 + if matched != test.wantMatched { 418 + t.Errorf("Config.MatchRule(%q) matched = %v, want %v", 419 + test.url, matched, test.wantMatched) 420 + } 421 + }) 422 + } 423 + }
-85
internal/routing/config.go
··· 1 - // SPDX-License-Identifier: GPL-3.0-or-later 2 - 3 - package routing 4 - 5 - type Config struct { 6 - PromptOnClick bool `toml:"prompt_on_click"` 7 - FavoriteBrowser string `toml:"favorite_browser"` 8 - HiddenBrowsers []string `toml:"hidden_browsers"` 9 - CheckDefaultBrowser bool `toml:"check_default_browser"` 10 - ShowAppNames bool `toml:"show_app_names"` 11 - ForceDarkMode bool `toml:"force_dark_mode"` 12 - StayAlive bool `toml:"stay_alive"` 13 - RemoveTrackingParameters bool `toml:"remove_tracking_parameters"` 14 - Redirections []Redirection `toml:"redirections,omitempty"` 15 - Rules []Rule `toml:"rules"` 16 - } 17 - 18 - type Redirection struct { 19 - Name string `toml:"name,omitempty"` 20 - Type string `toml:"type,omitempty"` // "domain", "wildcard", or "regex", defaults to "domain" 21 - Find string `toml:"find"` 22 - Replace string `toml:"replace"` 23 - } 24 - 25 - type Condition struct { 26 - Type string `toml:"type"` // "domain", "keyword", "glob", "regex" 27 - Pattern string `toml:"pattern"` 28 - Negate bool `toml:"negate,omitempty"` 29 - } 30 - 31 - type Rule struct { 32 - Name string `toml:"name"` 33 - Conditions []Condition `toml:"conditions"` 34 - Logic string `toml:"logic,omitempty"` // "all" or "any" 35 - Browser string `toml:"browser"` 36 - AlwaysAsk bool `toml:"always_ask"` 37 - } 38 - 39 - func NewDefaultConfig() *Config { 40 - return &Config{ 41 - PromptOnClick: true, 42 - CheckDefaultBrowser: true, 43 - ShowAppNames: false, 44 - ForceDarkMode: true, 45 - StayAlive: true, 46 - RemoveTrackingParameters: false, 47 - Rules: []Rule{}, 48 - } 49 - } 50 - 51 - func (cfg *Config) MatchRule(url string) (browserID string, alwaysAsk bool, matched bool) { 52 - for _, rule := range cfg.Rules { 53 - if rule.MatchesConditions(url) { 54 - return rule.Browser, rule.AlwaysAsk, true 55 - } 56 - } 57 - return "", false, false 58 - } 59 - 60 - func (rule *Rule) MatchesConditions(url string) bool { 61 - if len(rule.Conditions) == 0 { 62 - return false 63 - } 64 - 65 - logic := rule.Logic 66 - if logic == "" { 67 - logic = "all" 68 - } 69 - 70 - if logic == "all" { 71 - for _, condition := range rule.Conditions { 72 - if !matchesPattern(url, condition.Pattern, condition.Type, condition.Negate) { 73 - return false 74 - } 75 - } 76 - return true 77 - } 78 - 79 - for _, condition := range rule.Conditions { 80 - if matchesPattern(url, condition.Pattern, condition.Type, condition.Negate) { 81 - return true 82 - } 83 - } 84 - return false 85 - }
-685
internal/routing/config_test.go
··· 1 - // SPDX-License-Identifier: GPL-3.0-or-later 2 - 3 - package routing 4 - 5 - import ( 6 - "testing" 7 - ) 8 - 9 - func TestRuleMatchesConditions_AND(t *testing.T) { 10 - tests := []struct { 11 - name string 12 - rule Rule 13 - url string 14 - want bool 15 - }{ 16 - { 17 - name: "single condition matches", 18 - rule: Rule{ 19 - Logic: "all", 20 - Conditions: []Condition{ 21 - {Type: "domain", Pattern: "github.com"}, 22 - }, 23 - }, 24 - url: "https://github.com/user/repo", 25 - want: true, 26 - }, 27 - { 28 - name: "single condition no match", 29 - rule: Rule{ 30 - Logic: "all", 31 - Conditions: []Condition{ 32 - {Type: "domain", Pattern: "gitlab.com"}, 33 - }, 34 - }, 35 - url: "https://github.com/user/repo", 36 - want: false, 37 - }, 38 - { 39 - name: "multiple conditions all match", 40 - rule: Rule{ 41 - Logic: "all", 42 - Conditions: []Condition{ 43 - {Type: "domain", Pattern: "github.com"}, 44 - {Type: "keyword", Pattern: "user"}, 45 - }, 46 - }, 47 - url: "https://github.com/user/repo", 48 - want: true, 49 - }, 50 - { 51 - name: "multiple conditions one fails", 52 - rule: Rule{ 53 - Logic: "all", 54 - Conditions: []Condition{ 55 - {Type: "domain", Pattern: "github.com"}, 56 - {Type: "keyword", Pattern: "nonexistent"}, 57 - }, 58 - }, 59 - url: "https://github.com/user/repo", 60 - want: false, 61 - }, 62 - { 63 - name: "default logic (empty string) defaults to all", 64 - rule: Rule{ 65 - Logic: "", // Should default to "all" 66 - Conditions: []Condition{ 67 - {Type: "domain", Pattern: "github.com"}, 68 - {Type: "keyword", Pattern: "user"}, 69 - }, 70 - }, 71 - url: "https://github.com/user/repo", 72 - want: true, 73 - }, 74 - { 75 - name: "no conditions returns false", 76 - rule: Rule{ 77 - Logic: "all", 78 - Conditions: []Condition{}, 79 - }, 80 - url: "https://github.com/user/repo", 81 - want: false, 82 - }, 83 - { 84 - name: "three conditions all match", 85 - rule: Rule{ 86 - Logic: "all", 87 - Conditions: []Condition{ 88 - {Type: "domain", Pattern: "docs.github.com"}, 89 - {Type: "keyword", Pattern: "api"}, 90 - {Type: "keyword", Pattern: "reference"}, 91 - }, 92 - }, 93 - url: "https://docs.github.com/api/reference", 94 - want: true, 95 - }, 96 - { 97 - name: "negated condition excludes match", 98 - rule: Rule{ 99 - Logic: "all", 100 - Conditions: []Condition{ 101 - {Type: "glob", Pattern: "*.github.com"}, 102 - {Type: "domain", Pattern: "gist.github.com", Negate: true}, 103 - }, 104 - }, 105 - url: "https://gist.github.com", 106 - want: false, // Glob matches, but negated domain also matches so rule fails 107 - }, 108 - { 109 - name: "negated condition allows non-match", 110 - rule: Rule{ 111 - Logic: "all", 112 - Conditions: []Condition{ 113 - {Type: "glob", Pattern: "*.github.com"}, 114 - {Type: "domain", Pattern: "gist.github.com", Negate: true}, 115 - }, 116 - }, 117 - url: "https://api.github.com", 118 - want: true, // Glob matches, negated domain doesn't match (negate returns true) 119 - }, 120 - { 121 - name: "all negated conditions", 122 - rule: Rule{ 123 - Logic: "all", 124 - Conditions: []Condition{ 125 - {Type: "domain", Pattern: "example.com", Negate: true}, 126 - {Type: "keyword", Pattern: "admin", Negate: true}, 127 - }, 128 - }, 129 - url: "https://other.com/user", 130 - want: true, // Neither condition matches, so both negations return true 131 - }, 132 - { 133 - name: "negated condition fails when pattern not matched", 134 - rule: Rule{ 135 - Logic: "all", 136 - Conditions: []Condition{ 137 - {Type: "domain", Pattern: "github.com"}, 138 - {Type: "keyword", Pattern: "secret", Negate: true}, 139 - }, 140 - }, 141 - url: "https://github.com/user/repo", 142 - want: true, // Domain matches, "secret" not in URL so negation returns true 143 - }, 144 - } 145 - 146 - for _, tt := range tests { 147 - t.Run(tt.name, func(t *testing.T) { 148 - result := tt.rule.MatchesConditions(tt.url) 149 - if result != tt.want { 150 - t.Errorf("Rule.MatchesConditions(%q) = %v, want %v", tt.url, result, tt.want) 151 - } 152 - }) 153 - } 154 - } 155 - 156 - func TestRuleMatchesConditions_OR(t *testing.T) { 157 - tests := []struct { 158 - name string 159 - rule Rule 160 - url string 161 - want bool 162 - }{ 163 - { 164 - name: "first condition matches", 165 - rule: Rule{ 166 - Logic: "any", 167 - Conditions: []Condition{ 168 - {Type: "domain", Pattern: "github.com"}, 169 - {Type: "domain", Pattern: "gitlab.com"}, 170 - }, 171 - }, 172 - url: "https://github.com/user/repo", 173 - want: true, 174 - }, 175 - { 176 - name: "second condition matches", 177 - rule: Rule{ 178 - Logic: "any", 179 - Conditions: []Condition{ 180 - {Type: "domain", Pattern: "gitlab.com"}, 181 - {Type: "keyword", Pattern: "github"}, 182 - }, 183 - }, 184 - url: "https://github.com/user/repo", 185 - want: true, 186 - }, 187 - { 188 - name: "all conditions match", 189 - rule: Rule{ 190 - Logic: "any", 191 - Conditions: []Condition{ 192 - {Type: "domain", Pattern: "github.com"}, 193 - {Type: "keyword", Pattern: "github"}, 194 - }, 195 - }, 196 - url: "https://github.com/user/repo", 197 - want: true, 198 - }, 199 - { 200 - name: "no conditions match", 201 - rule: Rule{ 202 - Logic: "any", 203 - Conditions: []Condition{ 204 - {Type: "domain", Pattern: "gitlab.com"}, 205 - {Type: "keyword", Pattern: "bitbucket"}, 206 - }, 207 - }, 208 - url: "https://github.com/user/repo", 209 - want: false, 210 - }, 211 - { 212 - name: "last of many conditions matches", 213 - rule: Rule{ 214 - Logic: "any", 215 - Conditions: []Condition{ 216 - {Type: "domain", Pattern: "gitlab.com"}, 217 - {Type: "domain", Pattern: "bitbucket.com"}, 218 - {Type: "domain", Pattern: "github.com"}, 219 - }, 220 - }, 221 - url: "https://github.com/user/repo", 222 - want: true, 223 - }, 224 - { 225 - name: "mixed condition types one matches", 226 - rule: Rule{ 227 - Logic: "any", 228 - Conditions: []Condition{ 229 - {Type: "domain", Pattern: "gitlab.com"}, 230 - {Type: "regex", Pattern: "github\\.com/[a-z]+/"}, 231 - {Type: "glob", Pattern: "*.bitbucket.com"}, 232 - }, 233 - }, 234 - url: "https://github.com/user/repo", 235 - want: true, 236 - }, 237 - { 238 - name: "OR with negated condition matches", 239 - rule: Rule{ 240 - Logic: "any", 241 - Conditions: []Condition{ 242 - {Type: "domain", Pattern: "github.com", Negate: true}, 243 - {Type: "domain", Pattern: "gitlab.com"}, 244 - }, 245 - }, 246 - url: "https://example.com", 247 - want: true, // First condition: github.com doesn't match, negated = true 248 - }, 249 - { 250 - name: "OR with all negated conditions none match", 251 - rule: Rule{ 252 - Logic: "any", 253 - Conditions: []Condition{ 254 - {Type: "domain", Pattern: "github.com", Negate: true}, 255 - {Type: "domain", Pattern: "gitlab.com", Negate: true}, 256 - }, 257 - }, 258 - url: "https://github.com", 259 - want: true, // github.com matches, negated = false; gitlab.com doesn't match, negated = true 260 - }, 261 - } 262 - 263 - for _, tt := range tests { 264 - t.Run(tt.name, func(t *testing.T) { 265 - result := tt.rule.MatchesConditions(tt.url) 266 - if result != tt.want { 267 - t.Errorf("Rule.MatchesConditions(%q) = %v, want %v", tt.url, result, tt.want) 268 - } 269 - }) 270 - } 271 - } 272 - 273 - func TestConfigMatchRule(t *testing.T) { 274 - tests := []struct { 275 - name string 276 - config Config 277 - url string 278 - wantBrowserID string 279 - wantAlwaysAsk bool 280 - wantMatched bool 281 - }{ 282 - { 283 - name: "matches first rule", 284 - config: Config{ 285 - Rules: []Rule{ 286 - { 287 - Name: "GitHub", 288 - Browser: "firefox.desktop", 289 - AlwaysAsk: false, 290 - Conditions: []Condition{ 291 - {Type: "domain", Pattern: "github.com"}, 292 - }, 293 - }, 294 - { 295 - Name: "GitLab", 296 - Browser: "chrome.desktop", 297 - AlwaysAsk: false, 298 - Conditions: []Condition{ 299 - {Type: "domain", Pattern: "gitlab.com"}, 300 - }, 301 - }, 302 - }, 303 - }, 304 - url: "https://github.com/user/repo", 305 - wantBrowserID: "firefox.desktop", 306 - wantAlwaysAsk: false, 307 - wantMatched: true, 308 - }, 309 - { 310 - name: "matches second rule", 311 - config: Config{ 312 - Rules: []Rule{ 313 - { 314 - Name: "GitHub", 315 - Browser: "firefox.desktop", 316 - AlwaysAsk: false, 317 - Conditions: []Condition{ 318 - {Type: "domain", Pattern: "github.com"}, 319 - }, 320 - }, 321 - { 322 - Name: "GitLab", 323 - Browser: "chrome.desktop", 324 - AlwaysAsk: false, 325 - Conditions: []Condition{ 326 - {Type: "domain", Pattern: "gitlab.com"}, 327 - }, 328 - }, 329 - }, 330 - }, 331 - url: "https://gitlab.com/user/repo", 332 - wantBrowserID: "chrome.desktop", 333 - wantAlwaysAsk: false, 334 - wantMatched: true, 335 - }, 336 - { 337 - name: "no rules match", 338 - config: Config{ 339 - Rules: []Rule{ 340 - { 341 - Name: "GitHub", 342 - Browser: "firefox.desktop", 343 - AlwaysAsk: false, 344 - Conditions: []Condition{ 345 - {Type: "domain", Pattern: "github.com"}, 346 - }, 347 - }, 348 - }, 349 - }, 350 - url: "https://example.com", 351 - wantBrowserID: "", 352 - wantAlwaysAsk: false, 353 - wantMatched: false, 354 - }, 355 - { 356 - name: "rule with always_ask set", 357 - config: Config{ 358 - Rules: []Rule{ 359 - { 360 - Name: "Work Sites", 361 - Browser: "", 362 - AlwaysAsk: true, 363 - Conditions: []Condition{ 364 - {Type: "keyword", Pattern: "work"}, 365 - }, 366 - }, 367 - }, 368 - }, 369 - url: "https://work.example.com", 370 - wantBrowserID: "", 371 - wantAlwaysAsk: true, 372 - wantMatched: true, 373 - }, 374 - { 375 - name: "empty rules list", 376 - config: Config{ 377 - Rules: []Rule{}, 378 - }, 379 - url: "https://example.com", 380 - wantBrowserID: "", 381 - wantAlwaysAsk: false, 382 - wantMatched: false, 383 - }, 384 - { 385 - name: "first matching rule wins", 386 - config: Config{ 387 - Rules: []Rule{ 388 - { 389 - Name: "First Match", 390 - Browser: "first.desktop", 391 - AlwaysAsk: false, 392 - Conditions: []Condition{ 393 - {Type: "keyword", Pattern: "github"}, 394 - }, 395 - }, 396 - { 397 - Name: "Second Match", 398 - Browser: "second.desktop", 399 - AlwaysAsk: false, 400 - Conditions: []Condition{ 401 - {Type: "domain", Pattern: "github.com"}, 402 - }, 403 - }, 404 - }, 405 - }, 406 - url: "https://github.com", 407 - wantBrowserID: "first.desktop", 408 - wantAlwaysAsk: false, 409 - wantMatched: true, 410 - }, 411 - { 412 - name: "complex rule with AND logic matches", 413 - config: Config{ 414 - Rules: []Rule{ 415 - { 416 - Name: "GitHub API Docs", 417 - Browser: "work-browser.desktop", 418 - AlwaysAsk: false, 419 - Logic: "all", 420 - Conditions: []Condition{ 421 - {Type: "domain", Pattern: "docs.github.com"}, 422 - {Type: "keyword", Pattern: "api"}, 423 - }, 424 - }, 425 - }, 426 - }, 427 - url: "https://docs.github.com/api/reference", 428 - wantBrowserID: "work-browser.desktop", 429 - wantAlwaysAsk: false, 430 - wantMatched: true, 431 - }, 432 - { 433 - name: "complex rule with OR logic matches", 434 - config: Config{ 435 - Rules: []Rule{ 436 - { 437 - Name: "Git Platforms", 438 - Browser: "dev-browser.desktop", 439 - AlwaysAsk: false, 440 - Logic: "any", 441 - Conditions: []Condition{ 442 - {Type: "domain", Pattern: "github.com"}, 443 - {Type: "domain", Pattern: "gitlab.com"}, 444 - {Type: "domain", Pattern: "bitbucket.org"}, 445 - }, 446 - }, 447 - }, 448 - }, 449 - url: "https://bitbucket.org/user/repo", 450 - wantBrowserID: "dev-browser.desktop", 451 - wantAlwaysAsk: false, 452 - wantMatched: true, 453 - }, 454 - } 455 - 456 - for _, tt := range tests { 457 - t.Run(tt.name, func(t *testing.T) { 458 - browserID, alwaysAsk, matched := tt.config.MatchRule(tt.url) 459 - if browserID != tt.wantBrowserID { 460 - t.Errorf("Config.MatchRule(%q) browserID = %q, want %q", tt.url, browserID, tt.wantBrowserID) 461 - } 462 - if alwaysAsk != tt.wantAlwaysAsk { 463 - t.Errorf("Config.MatchRule(%q) alwaysAsk = %v, want %v", tt.url, alwaysAsk, tt.wantAlwaysAsk) 464 - } 465 - if matched != tt.wantMatched { 466 - t.Errorf("Config.MatchRule(%q) matched = %v, want %v", tt.url, matched, tt.wantMatched) 467 - } 468 - }) 469 - } 470 - } 471 - 472 - func TestConfigMatchRule_RuleOrdering(t *testing.T) { 473 - tests := []struct { 474 - name string 475 - config Config 476 - url string 477 - wantBrowserID string 478 - wantMatched bool 479 - }{ 480 - { 481 - name: "first matching rule wins", 482 - config: Config{ 483 - Rules: []Rule{ 484 - { 485 - Name: "First Rule", 486 - Browser: "first-browser.desktop", 487 - Logic: "all", 488 - Conditions: []Condition{ 489 - {Type: "domain", Pattern: "example.com"}, 490 - }, 491 - }, 492 - { 493 - Name: "Second Rule", 494 - Browser: "second-browser.desktop", 495 - Logic: "all", 496 - Conditions: []Condition{ 497 - {Type: "domain", Pattern: "example.com"}, 498 - }, 499 - }, 500 - }, 501 - }, 502 - url: "https://example.com/path", 503 - wantBrowserID: "first-browser.desktop", 504 - wantMatched: true, 505 - }, 506 - { 507 - name: "more specific rule first", 508 - config: Config{ 509 - Rules: []Rule{ 510 - { 511 - Name: "Specific Rule", 512 - Browser: "specific-browser.desktop", 513 - Logic: "all", 514 - Conditions: []Condition{ 515 - {Type: "domain", Pattern: "docs.example.com"}, 516 - }, 517 - }, 518 - { 519 - Name: "General Rule", 520 - Browser: "general-browser.desktop", 521 - Logic: "all", 522 - Conditions: []Condition{ 523 - {Type: "glob", Pattern: "*.example.com"}, 524 - }, 525 - }, 526 - }, 527 - }, 528 - url: "https://docs.example.com/guide", 529 - wantBrowserID: "specific-browser.desktop", 530 - wantMatched: true, 531 - }, 532 - { 533 - name: "general rule matches when specific doesn't", 534 - config: Config{ 535 - Rules: []Rule{ 536 - { 537 - Name: "Specific Rule", 538 - Browser: "specific-browser.desktop", 539 - Logic: "all", 540 - Conditions: []Condition{ 541 - {Type: "domain", Pattern: "docs.example.com"}, 542 - }, 543 - }, 544 - { 545 - Name: "General Rule", 546 - Browser: "general-browser.desktop", 547 - Logic: "all", 548 - Conditions: []Condition{ 549 - {Type: "glob", Pattern: "*.example.com"}, 550 - }, 551 - }, 552 - }, 553 - }, 554 - url: "https://api.example.com/endpoint", 555 - wantBrowserID: "general-browser.desktop", 556 - wantMatched: true, 557 - }, 558 - { 559 - name: "non-matching rules are skipped", 560 - config: Config{ 561 - Rules: []Rule{ 562 - { 563 - Name: "Non-matching Rule", 564 - Browser: "wrong-browser.desktop", 565 - Logic: "all", 566 - Conditions: []Condition{ 567 - {Type: "domain", Pattern: "other.com"}, 568 - }, 569 - }, 570 - { 571 - Name: "Matching Rule", 572 - Browser: "correct-browser.desktop", 573 - Logic: "all", 574 - Conditions: []Condition{ 575 - {Type: "domain", Pattern: "example.com"}, 576 - }, 577 - }, 578 - }, 579 - }, 580 - url: "https://example.com/path", 581 - wantBrowserID: "correct-browser.desktop", 582 - wantMatched: true, 583 - }, 584 - { 585 - name: "empty rules list", 586 - config: Config{ 587 - Rules: []Rule{}, 588 - }, 589 - url: "https://example.com/path", 590 - wantBrowserID: "", 591 - wantMatched: false, 592 - }, 593 - { 594 - name: "all rules fail to match", 595 - config: Config{ 596 - Rules: []Rule{ 597 - { 598 - Name: "Rule 1", 599 - Browser: "browser1.desktop", 600 - Logic: "all", 601 - Conditions: []Condition{ 602 - {Type: "domain", Pattern: "other1.com"}, 603 - }, 604 - }, 605 - { 606 - Name: "Rule 2", 607 - Browser: "browser2.desktop", 608 - Logic: "all", 609 - Conditions: []Condition{ 610 - {Type: "domain", Pattern: "other2.com"}, 611 - }, 612 - }, 613 - }, 614 - }, 615 - url: "https://example.com/path", 616 - wantBrowserID: "", 617 - wantMatched: false, 618 - }, 619 - { 620 - name: "rule with empty conditions is skipped", 621 - config: Config{ 622 - Rules: []Rule{ 623 - { 624 - Name: "Empty Rule", 625 - Browser: "empty-browser.desktop", 626 - Logic: "all", 627 - Conditions: []Condition{}, 628 - }, 629 - { 630 - Name: "Valid Rule", 631 - Browser: "valid-browser.desktop", 632 - Logic: "all", 633 - Conditions: []Condition{ 634 - {Type: "domain", Pattern: "example.com"}, 635 - }, 636 - }, 637 - }, 638 - }, 639 - url: "https://example.com/path", 640 - wantBrowserID: "valid-browser.desktop", 641 - wantMatched: true, 642 - }, 643 - { 644 - name: "AND rule fails partial match, next rule matches", 645 - config: Config{ 646 - Rules: []Rule{ 647 - { 648 - Name: "AND Rule", 649 - Browser: "and-browser.desktop", 650 - Logic: "all", 651 - Conditions: []Condition{ 652 - {Type: "domain", Pattern: "example.com"}, 653 - {Type: "keyword", Pattern: "admin"}, // won't match 654 - }, 655 - }, 656 - { 657 - Name: "Fallback Rule", 658 - Browser: "fallback-browser.desktop", 659 - Logic: "all", 660 - Conditions: []Condition{ 661 - {Type: "domain", Pattern: "example.com"}, 662 - }, 663 - }, 664 - }, 665 - }, 666 - url: "https://example.com/user", 667 - wantBrowserID: "fallback-browser.desktop", 668 - wantMatched: true, 669 - }, 670 - } 671 - 672 - for _, tt := range tests { 673 - t.Run(tt.name, func(t *testing.T) { 674 - browserID, _, matched := tt.config.MatchRule(tt.url) 675 - if browserID != tt.wantBrowserID { 676 - t.Errorf("Config.MatchRule(%q) browserID = %q, want %q", 677 - tt.url, browserID, tt.wantBrowserID) 678 - } 679 - if matched != tt.wantMatched { 680 - t.Errorf("Config.MatchRule(%q) matched = %v, want %v", 681 - tt.url, matched, tt.wantMatched) 682 - } 683 - }) 684 - } 685 - }
+51
internal/routing/model.go
··· 1 + // SPDX-License-Identifier: GPL-3.0-or-later 2 + 3 + package routing 4 + 5 + type Redirection struct { 6 + Name string `toml:"name,omitempty"` 7 + Type string `toml:"type,omitempty"` 8 + Find string `toml:"find"` 9 + Replace string `toml:"replace"` 10 + } 11 + 12 + type Condition struct { 13 + Type string `toml:"type"` 14 + Pattern string `toml:"pattern"` 15 + Negate bool `toml:"negate,omitempty"` 16 + } 17 + 18 + type Rule struct { 19 + Name string `toml:"name"` 20 + Conditions []Condition `toml:"conditions"` 21 + Logic string `toml:"logic,omitempty"` 22 + Browser string `toml:"browser"` 23 + AlwaysAsk bool `toml:"always_ask"` 24 + } 25 + 26 + func (rule *Rule) MatchesConditions(url string) bool { 27 + if len(rule.Conditions) == 0 { 28 + return false 29 + } 30 + 31 + logic := rule.Logic 32 + if logic == "" { 33 + logic = "all" 34 + } 35 + 36 + if logic == "all" { 37 + for _, condition := range rule.Conditions { 38 + if !matchesPattern(url, condition.Pattern, condition.Type, condition.Negate) { 39 + return false 40 + } 41 + } 42 + return true 43 + } 44 + 45 + for _, condition := range rule.Conditions { 46 + if matchesPattern(url, condition.Pattern, condition.Type, condition.Negate) { 47 + return true 48 + } 49 + } 50 + return false 51 + }
+271
internal/routing/model_test.go
··· 1 + // SPDX-License-Identifier: GPL-3.0-or-later 2 + 3 + package routing 4 + 5 + import ( 6 + "testing" 7 + ) 8 + 9 + func TestRuleMatchesConditions_AND(t *testing.T) { 10 + tests := []struct { 11 + name string 12 + rule Rule 13 + url string 14 + want bool 15 + }{ 16 + { 17 + name: "single condition matches", 18 + rule: Rule{ 19 + Logic: "all", 20 + Conditions: []Condition{ 21 + {Type: "domain", Pattern: "github.com"}, 22 + }, 23 + }, 24 + url: "https://github.com/user/repo", 25 + want: true, 26 + }, 27 + { 28 + name: "single condition no match", 29 + rule: Rule{ 30 + Logic: "all", 31 + Conditions: []Condition{ 32 + {Type: "domain", Pattern: "gitlab.com"}, 33 + }, 34 + }, 35 + url: "https://github.com/user/repo", 36 + want: false, 37 + }, 38 + { 39 + name: "multiple conditions all match", 40 + rule: Rule{ 41 + Logic: "all", 42 + Conditions: []Condition{ 43 + {Type: "domain", Pattern: "github.com"}, 44 + {Type: "keyword", Pattern: "user"}, 45 + }, 46 + }, 47 + url: "https://github.com/user/repo", 48 + want: true, 49 + }, 50 + { 51 + name: "multiple conditions one fails", 52 + rule: Rule{ 53 + Logic: "all", 54 + Conditions: []Condition{ 55 + {Type: "domain", Pattern: "github.com"}, 56 + {Type: "keyword", Pattern: "nonexistent"}, 57 + }, 58 + }, 59 + url: "https://github.com/user/repo", 60 + want: false, 61 + }, 62 + { 63 + name: "default logic (empty string) defaults to all", 64 + rule: Rule{ 65 + Logic: "", 66 + Conditions: []Condition{ 67 + {Type: "domain", Pattern: "github.com"}, 68 + {Type: "keyword", Pattern: "user"}, 69 + }, 70 + }, 71 + url: "https://github.com/user/repo", 72 + want: true, 73 + }, 74 + { 75 + name: "no conditions returns false", 76 + rule: Rule{ 77 + Logic: "all", 78 + Conditions: []Condition{}, 79 + }, 80 + url: "https://github.com/user/repo", 81 + want: false, 82 + }, 83 + { 84 + name: "three conditions all match", 85 + rule: Rule{ 86 + Logic: "all", 87 + Conditions: []Condition{ 88 + {Type: "domain", Pattern: "docs.github.com"}, 89 + {Type: "keyword", Pattern: "api"}, 90 + {Type: "keyword", Pattern: "reference"}, 91 + }, 92 + }, 93 + url: "https://docs.github.com/api/reference", 94 + want: true, 95 + }, 96 + { 97 + name: "negated condition excludes match", 98 + rule: Rule{ 99 + Logic: "all", 100 + Conditions: []Condition{ 101 + {Type: "glob", Pattern: "*.github.com"}, 102 + {Type: "domain", Pattern: "gist.github.com", Negate: true}, 103 + }, 104 + }, 105 + url: "https://gist.github.com", 106 + want: false, 107 + }, 108 + { 109 + name: "negated condition allows non-match", 110 + rule: Rule{ 111 + Logic: "all", 112 + Conditions: []Condition{ 113 + {Type: "glob", Pattern: "*.github.com"}, 114 + {Type: "domain", Pattern: "gist.github.com", Negate: true}, 115 + }, 116 + }, 117 + url: "https://api.github.com", 118 + want: true, 119 + }, 120 + { 121 + name: "all negated conditions", 122 + rule: Rule{ 123 + Logic: "all", 124 + Conditions: []Condition{ 125 + {Type: "domain", Pattern: "example.com", Negate: true}, 126 + {Type: "keyword", Pattern: "admin", Negate: true}, 127 + }, 128 + }, 129 + url: "https://other.com/user", 130 + want: true, 131 + }, 132 + { 133 + name: "negated condition fails when pattern not matched", 134 + rule: Rule{ 135 + Logic: "all", 136 + Conditions: []Condition{ 137 + {Type: "domain", Pattern: "github.com"}, 138 + {Type: "keyword", Pattern: "secret", Negate: true}, 139 + }, 140 + }, 141 + url: "https://github.com/user/repo", 142 + want: true, 143 + }, 144 + } 145 + 146 + for _, test := range tests { 147 + t.Run(test.name, func(t *testing.T) { 148 + result := test.rule.MatchesConditions(test.url) 149 + if result != test.want { 150 + t.Errorf("Rule.MatchesConditions(%q) = %v, want %v", test.url, result, test.want) 151 + } 152 + }) 153 + } 154 + } 155 + 156 + func TestRuleMatchesConditions_OR(t *testing.T) { 157 + tests := []struct { 158 + name string 159 + rule Rule 160 + url string 161 + want bool 162 + }{ 163 + { 164 + name: "first condition matches", 165 + rule: Rule{ 166 + Logic: "any", 167 + Conditions: []Condition{ 168 + {Type: "domain", Pattern: "github.com"}, 169 + {Type: "domain", Pattern: "gitlab.com"}, 170 + }, 171 + }, 172 + url: "https://github.com/user/repo", 173 + want: true, 174 + }, 175 + { 176 + name: "second condition matches", 177 + rule: Rule{ 178 + Logic: "any", 179 + Conditions: []Condition{ 180 + {Type: "domain", Pattern: "gitlab.com"}, 181 + {Type: "keyword", Pattern: "github"}, 182 + }, 183 + }, 184 + url: "https://github.com/user/repo", 185 + want: true, 186 + }, 187 + { 188 + name: "all conditions match", 189 + rule: Rule{ 190 + Logic: "any", 191 + Conditions: []Condition{ 192 + {Type: "domain", Pattern: "github.com"}, 193 + {Type: "keyword", Pattern: "github"}, 194 + }, 195 + }, 196 + url: "https://github.com/user/repo", 197 + want: true, 198 + }, 199 + { 200 + name: "no conditions match", 201 + rule: Rule{ 202 + Logic: "any", 203 + Conditions: []Condition{ 204 + {Type: "domain", Pattern: "gitlab.com"}, 205 + {Type: "keyword", Pattern: "bitbucket"}, 206 + }, 207 + }, 208 + url: "https://github.com/user/repo", 209 + want: false, 210 + }, 211 + { 212 + name: "last of many conditions matches", 213 + rule: Rule{ 214 + Logic: "any", 215 + Conditions: []Condition{ 216 + {Type: "domain", Pattern: "gitlab.com"}, 217 + {Type: "domain", Pattern: "bitbucket.com"}, 218 + {Type: "domain", Pattern: "github.com"}, 219 + }, 220 + }, 221 + url: "https://github.com/user/repo", 222 + want: true, 223 + }, 224 + { 225 + name: "mixed condition types one matches", 226 + rule: Rule{ 227 + Logic: "any", 228 + Conditions: []Condition{ 229 + {Type: "domain", Pattern: "gitlab.com"}, 230 + {Type: "regex", Pattern: "github\\.com/[a-z]+/"}, 231 + {Type: "glob", Pattern: "*.bitbucket.com"}, 232 + }, 233 + }, 234 + url: "https://github.com/user/repo", 235 + want: true, 236 + }, 237 + { 238 + name: "OR with negated condition matches", 239 + rule: Rule{ 240 + Logic: "any", 241 + Conditions: []Condition{ 242 + {Type: "domain", Pattern: "github.com", Negate: true}, 243 + {Type: "domain", Pattern: "gitlab.com"}, 244 + }, 245 + }, 246 + url: "https://example.com", 247 + want: true, 248 + }, 249 + { 250 + name: "OR with all negated conditions none match", 251 + rule: Rule{ 252 + Logic: "any", 253 + Conditions: []Condition{ 254 + {Type: "domain", Pattern: "github.com", Negate: true}, 255 + {Type: "domain", Pattern: "gitlab.com", Negate: true}, 256 + }, 257 + }, 258 + url: "https://github.com", 259 + want: true, 260 + }, 261 + } 262 + 263 + for _, test := range tests { 264 + t.Run(test.name, func(t *testing.T) { 265 + result := test.rule.MatchesConditions(test.url) 266 + if result != test.want { 267 + t.Errorf("Rule.MatchesConditions(%q) = %v, want %v", test.url, result, test.want) 268 + } 269 + }) 270 + } 271 + }
+5 -8
justfile
··· 40 40 41 41 # Update the bundled AdGuard tracking-parameter filter list 42 42 update-adguard: 43 - curl -fsSL https://raw.githubusercontent.com/AdguardTeam/AdGuardFilters/master/TrackParamFilter/sections/general_url.txt -o cmd/switchyard/embedded/adguard_filter.txt 44 - @echo "Updated cmd/switchyard/embedded/adguard_filter.txt" 43 + curl -fsSL https://raw.githubusercontent.com/AdguardTeam/AdGuardFilters/master/TrackParamFilter/sections/general_url.txt -o internal/routing/embedded/adguard_filter.txt 44 + @echo "Updated internal/routing/embedded/adguard_filter.txt" 45 45 46 46 # Set as default browser 47 47 set-default: ··· 59 59 go test -v ./internal/routing 60 60 61 61 test-config: 62 - go test -v ./cmd/switchyard/config_compat_test.go ./cmd/switchyard/app.go ./cmd/switchyard/config.go 62 + go test -v ./internal/config 63 63 64 - test-sanitizer: 65 - go test -v ./cmd/switchyard/sanitizer_test.go ./cmd/switchyard/sanitizer.go 66 - 67 - test: test-routing test-config test-sanitizer 64 + test: test-routing test-config 68 65 69 66 # Run tests with coverage report 70 67 test-routing-coverage: ··· 74 71 @echo "" 75 72 @echo "To view HTML coverage report, run: go tool cover -html=coverage.out" 76 73 77 - test-coverage: test-config test-sanitizer test-routing-coverage 74 + test-coverage: test-config test-routing-coverage 78 75 79 76 # Build and install Flatpak (development version) 80 77 flatpak: