From 6bca8bb017c8557af2ff155b0530e1a134bbc610 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Thu, 15 Jan 2026 01:08:47 -0500 Subject: [PATCH] add validation tests --- .github/workflows/ci.yml | 6 + README.md | 2 + justfile | 4 +- src/config_test.go | 6 + src/validation_test.go | 525 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 src/validation_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d204155..c88c99d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,12 @@ jobs: run: just test - name: Generate coverage report run: just test-coverage + - name: Display coverage summary + run: | + echo "## Test Coverage Summary" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY - name: Upload coverage report uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 37ef406..05485f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Switchyard +[![CI](https://github.com/alyraffauf/switchyard/actions/workflows/ci.yml/badge.svg)](https://github.com/alyraffauf/switchyard/actions/workflows/ci.yml) + **A rules-based URL router for Linux.** When you click a link, Switchyard automatically opens it in the right browser based on your rules, or shows a quick picker to let you choose.

diff --git a/justfile b/justfile index b442d1c..c412a40 100644 --- a/justfile +++ b/justfile @@ -52,12 +52,12 @@ vendor: # Run unit tests test: @echo "Running unit tests..." - go test -v ./src/config_test.go ./src/config.go + go test -v ./src/config_test.go ./src/validation_test.go ./src/config.go ./src/validation.go # Run tests with coverage report test-coverage: @echo "Running tests with coverage..." - go test -coverprofile=coverage.out ./src/config_test.go ./src/config.go + go test -coverprofile=coverage.out ./src/config_test.go ./src/validation_test.go ./src/config.go ./src/validation.go go tool cover -func=coverage.out @echo "" @echo "To view HTML coverage report, run: go tool cover -html=coverage.out" diff --git a/src/config_test.go b/src/config_test.go index 7fc5cb1..a6ec146 100644 --- a/src/config_test.go +++ b/src/config_test.go @@ -208,6 +208,12 @@ func TestMatchGlob(t *testing.T) { pattern: "*.example.*", want: true, }, + { + name: "invalid pattern causes regex error", + url: "https://example.com", + pattern: "[invalid", + want: false, + }, } for _, tt := range tests { diff --git a/src/validation_test.go b/src/validation_test.go new file mode 100644 index 0000000..95464df --- /dev/null +++ b/src/validation_test.go @@ -0,0 +1,525 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package main + +import ( + "testing" +) + +// TestValidateDomainPattern tests domain pattern validation +func TestValidateDomainPattern(t *testing.T) { + tests := []struct { + name string + pattern string + wantErr bool + errMsg string + }{ + { + name: "valid simple domain", + pattern: "example.com", + wantErr: false, + }, + { + name: "valid subdomain", + pattern: "api.example.com", + wantErr: false, + }, + { + name: "valid multiple subdomains", + pattern: "deep.api.example.com", + wantErr: false, + }, + { + name: "valid domain with hyphen", + pattern: "my-site.example.com", + wantErr: false, + }, + { + name: "valid domain with numbers", + pattern: "site123.example.com", + wantErr: false, + }, + { + name: "valid domain with underscore", + pattern: "my_site.example.com", + wantErr: false, + }, + { + name: "empty domain", + pattern: "", + wantErr: true, + errMsg: "Domain cannot be empty", + }, + { + name: "domain with wildcard asterisk", + pattern: "*.example.com", + wantErr: true, + errMsg: "Wildcards not allowed in domain patterns (use Wildcard type instead)", + }, + { + name: "domain with wildcard question mark", + pattern: "example?.com", + wantErr: true, + errMsg: "Wildcards not allowed in domain patterns (use Wildcard type instead)", + }, + { + name: "domain with space", + pattern: "example .com", + wantErr: true, + errMsg: "Domain cannot contain spaces", + }, + { + name: "domain starting with dot", + pattern: ".example.com", + wantErr: true, + errMsg: "Domain cannot start or end with a dot", + }, + { + name: "domain ending with dot", + pattern: "example.com.", + wantErr: true, + errMsg: "Domain cannot start or end with a dot", + }, + { + name: "domain starting with hyphen", + pattern: "-example.com", + wantErr: true, + errMsg: "Domain cannot start or end with a hyphen", + }, + { + name: "domain ending with hyphen", + pattern: "example.com-", + wantErr: true, + errMsg: "Domain cannot start or end with a hyphen", + }, + { + name: "domain with slash", + pattern: "example.com/path", + wantErr: true, + errMsg: "Domain contains invalid character: /", + }, + { + name: "domain with special character", + pattern: "example@test.com", + wantErr: true, + errMsg: "Domain contains invalid character: @", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDomainPattern(tt.pattern) + if tt.wantErr { + if err == nil { + t.Errorf("validateDomainPattern(%q) expected error but got nil", tt.pattern) + } else if tt.errMsg != "" && err.Error() != tt.errMsg { + t.Errorf("validateDomainPattern(%q) error = %q, want %q", tt.pattern, err.Error(), tt.errMsg) + } + } else { + if err != nil { + t.Errorf("validateDomainPattern(%q) unexpected error: %v", tt.pattern, err) + } + } + }) + } +} + +// TestValidateGlobPattern tests wildcard pattern validation +func TestValidateGlobPattern(t *testing.T) { + tests := []struct { + name string + pattern string + wantErr bool + errMsg string + }{ + { + name: "valid wildcard at start", + pattern: "*.example.com", + wantErr: false, + }, + { + name: "valid wildcard at end", + pattern: "example.*", + wantErr: false, + }, + { + name: "valid wildcard in middle", + pattern: "api.*.example.com", + wantErr: false, + }, + { + name: "valid multiple wildcards", + pattern: "*.*.example.com", + wantErr: false, + }, + { + name: "valid domain with hyphen and wildcard", + pattern: "my-*.example.com", + wantErr: false, + }, + { + name: "valid domain with numbers and wildcard", + pattern: "site*.example.com", + wantErr: false, + }, + { + name: "empty pattern", + pattern: "", + wantErr: true, + errMsg: "Wildcard pattern cannot be empty", + }, + { + name: "pattern with space", + pattern: "* .example.com", + wantErr: true, + errMsg: "Wildcard pattern cannot contain spaces", + }, + { + name: "pattern starting with dot (not wildcard)", + pattern: ".example.com", + wantErr: true, + errMsg: "Wildcard pattern cannot start with a dot", + }, + { + name: "pattern ending with dot (not wildcard)", + pattern: "example.com.", + wantErr: true, + errMsg: "Wildcard pattern cannot end with a dot", + }, + { + name: "pattern with special character", + pattern: "example@*.com", + wantErr: true, + errMsg: "Wildcard pattern contains invalid character: @", + }, + { + name: "pattern with slash", + pattern: "example.com/*", + wantErr: true, + errMsg: "Wildcard pattern contains invalid character: /", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateGlobPattern(tt.pattern) + if tt.wantErr { + if err == nil { + t.Errorf("validateGlobPattern(%q) expected error but got nil", tt.pattern) + } else if tt.errMsg != "" && err.Error() != tt.errMsg { + t.Errorf("validateGlobPattern(%q) error = %q, want %q", tt.pattern, err.Error(), tt.errMsg) + } + } else { + if err != nil { + t.Errorf("validateGlobPattern(%q) unexpected error: %v", tt.pattern, err) + } + } + }) + } +} + +// TestValidateConditionPattern tests the main condition pattern validator +func TestValidateConditionPattern(t *testing.T) { + tests := []struct { + name string + condType string + pattern string + wantErr bool + }{ + // Domain type + { + name: "valid domain type", + condType: "domain", + pattern: "example.com", + wantErr: false, + }, + { + name: "invalid domain type - wildcard", + condType: "domain", + pattern: "*.example.com", + wantErr: true, + }, + // Keyword type + { + name: "valid keyword type", + condType: "keyword", + pattern: "github", + wantErr: false, + }, + { + name: "valid keyword with special chars", + condType: "keyword", + pattern: "/api/v2/", + wantErr: false, + }, + { + name: "empty keyword", + condType: "keyword", + pattern: "", + wantErr: true, + }, + // Glob type + { + name: "valid glob type", + condType: "glob", + pattern: "*.example.com", + wantErr: false, + }, + { + name: "invalid glob type - spaces", + condType: "glob", + pattern: "* .example.com", + wantErr: true, + }, + // Regex type + { + name: "valid regex type", + condType: "regex", + pattern: "^https://.*\\.example\\.com", + wantErr: false, + }, + { + name: "invalid regex type - bad syntax", + condType: "regex", + pattern: "[invalid", + wantErr: true, + }, + { + name: "empty regex", + condType: "regex", + pattern: "", + wantErr: true, + }, + // Empty pattern universal check + { + name: "empty pattern for domain", + condType: "domain", + pattern: "", + wantErr: true, + }, + { + name: "empty pattern for glob", + condType: "glob", + pattern: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateConditionPattern(tt.condType, tt.pattern) + if tt.wantErr && err == nil { + t.Errorf("validateConditionPattern(%q, %q) expected error but got nil", tt.condType, tt.pattern) + } + if !tt.wantErr && err != nil { + t.Errorf("validateConditionPattern(%q, %q) unexpected error: %v", tt.condType, tt.pattern, err) + } + }) + } +} + +// TestIsConditionValid tests single condition validation +func TestIsConditionValid(t *testing.T) { + tests := []struct { + name string + condition Condition + want bool + }{ + { + name: "valid domain condition", + condition: Condition{ + Type: "domain", + Pattern: "example.com", + }, + want: true, + }, + { + name: "valid keyword condition", + condition: Condition{ + Type: "keyword", + Pattern: "github", + }, + want: true, + }, + { + name: "valid glob condition", + condition: Condition{ + Type: "glob", + Pattern: "*.example.com", + }, + want: true, + }, + { + name: "valid regex condition", + condition: Condition{ + Type: "regex", + Pattern: "^https://.*", + }, + want: true, + }, + { + name: "invalid domain condition - wildcard", + condition: Condition{ + Type: "domain", + Pattern: "*.example.com", + }, + want: false, + }, + { + name: "invalid condition - empty pattern", + condition: Condition{ + Type: "domain", + Pattern: "", + }, + want: false, + }, + { + name: "invalid regex condition", + condition: Condition{ + Type: "regex", + Pattern: "[invalid", + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isConditionValid(tt.condition) + if got != tt.want { + t.Errorf("isConditionValid(%v) = %v, want %v", tt.condition, got, tt.want) + } + }) + } +} + +// TestAreAllConditionsValid tests validation of condition slices +func TestAreAllConditionsValid(t *testing.T) { + tests := []struct { + name string + conditions []Condition + want bool + }{ + { + name: "all valid conditions", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + {Type: "keyword", Pattern: "github"}, + }, + want: true, + }, + { + name: "one invalid condition", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + {Type: "domain", Pattern: "*.invalid.com"}, + }, + want: false, + }, + { + name: "empty pattern in list", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + {Type: "keyword", Pattern: ""}, + }, + want: false, + }, + { + name: "empty conditions list", + conditions: []Condition{}, + want: false, + }, + { + name: "nil conditions list", + conditions: nil, + want: false, + }, + { + name: "single valid condition", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + }, + want: true, + }, + { + name: "invalid regex in list", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + {Type: "regex", Pattern: "[invalid"}, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := areAllConditionsValid(tt.conditions) + if got != tt.want { + t.Errorf("areAllConditionsValid(%v) = %v, want %v", tt.conditions, got, tt.want) + } + }) + } +} + +// TestValidateConditions tests the main validateConditions function +func TestValidateConditions(t *testing.T) { + tests := []struct { + name string + conditions []Condition + want bool + }{ + { + name: "valid conditions with all types", + conditions: []Condition{ + {Type: "domain", Pattern: "example.com"}, + {Type: "keyword", Pattern: "test"}, + {Type: "glob", Pattern: "*.example.com"}, + {Type: "regex", Pattern: "^https://.*"}, + }, + want: true, + }, + { + name: "invalid type", + conditions: []Condition{ + {Type: "invalid_type", Pattern: "example.com"}, + }, + want: false, + }, + { + name: "empty pattern", + conditions: []Condition{ + {Type: "domain", Pattern: ""}, + }, + want: false, + }, + { + name: "invalid regex pattern", + conditions: []Condition{ + {Type: "regex", Pattern: "[unclosed"}, + }, + want: false, + }, + { + name: "empty list", + conditions: []Condition{}, + want: true, + }, + { + name: "mixed valid and invalid", + conditions: []Condition{ + {Type: "domain", Pattern: "valid.com"}, + {Type: "domain", Pattern: ""}, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validateConditions(tt.conditions) + if got != tt.want { + t.Errorf("validateConditions(%v) = %v, want %v", tt.conditions, got, tt.want) + } + }) + } +} -- 2.51.2