test: add comprehensive tests for auth provider

- Test Cookie.Parse edge cases (equals in value, spaces, empty fields)
- Test Secret validation for all auth types
- Test file loading with different extensions (case-insensitive)
- Test all auth strategies (Apply and ApplyOnRR methods)
- Test FileAuthProvider domain lookup (exact and regex)
- Test MultiAuthProvider delegation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Doğan Can Bakır
2026-01-12 14:33:44 +03:00
parent 62eed15d6d
commit a780d059ee
3 changed files with 879 additions and 0 deletions
+408
View File
@@ -0,0 +1,408 @@
package authx
import (
"os"
"path/filepath"
"testing"
)
func TestCookieParse(t *testing.T) {
tests := []struct {
name string
raw string
wantKey string
wantValue string
wantErr bool
}{
{
name: "simple cookie",
raw: "session=abc123",
wantKey: "session",
wantValue: "abc123",
wantErr: false,
},
{
name: "cookie with Set-Cookie prefix",
raw: "Set-Cookie: session=abc123; Path=/",
wantKey: "session",
wantValue: "abc123",
wantErr: false,
},
{
name: "cookie with equals in value",
raw: "token=eyJhbGciOiJIUzI1NiJ9==; Path=/",
wantKey: "token",
wantValue: "eyJhbGciOiJIUzI1NiJ9==",
wantErr: false,
},
{
name: "cookie with spaces",
raw: " session = abc123 ; Path=/",
wantKey: "session",
wantValue: "abc123",
wantErr: false,
},
{
name: "empty raw",
raw: "",
wantErr: true,
},
{
name: "missing equals",
raw: "sessionabc123; Path=/",
wantErr: true,
},
{
name: "empty key",
raw: "=abc123; Path=/",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Cookie{Raw: tt.raw}
err := c.Parse()
if (err != nil) != tt.wantErr {
t.Errorf("Cookie.Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if c.Key != tt.wantKey {
t.Errorf("Cookie.Parse() Key = %v, want %v", c.Key, tt.wantKey)
}
if c.Value != tt.wantValue {
t.Errorf("Cookie.Parse() Value = %v, want %v", c.Value, tt.wantValue)
}
}
})
}
}
func TestSecretValidate(t *testing.T) {
tests := []struct {
name string
secret Secret
wantErr bool
}{
{
name: "valid basic auth",
secret: Secret{
Type: "BasicAuth",
Domains: []string{"example.com"},
Username: "user",
Password: "pass",
},
wantErr: false,
},
{
name: "valid bearer token",
secret: Secret{
Type: "BearerToken",
Domains: []string{"example.com"},
Token: "abc123",
},
wantErr: false,
},
{
name: "valid header auth",
secret: Secret{
Type: "Header",
Domains: []string{"example.com"},
Headers: []KV{{Key: "X-API-Key", Value: "secret"}},
},
wantErr: false,
},
{
name: "valid cookie auth",
secret: Secret{
Type: "Cookie",
Domains: []string{"example.com"},
Cookies: []Cookie{{Key: "session", Value: "abc123"}},
},
wantErr: false,
},
{
name: "valid query auth",
secret: Secret{
Type: "Query",
Domains: []string{"example.com"},
Params: []KV{{Key: "api_key", Value: "secret"}},
},
wantErr: false,
},
{
name: "invalid type",
secret: Secret{
Type: "InvalidType",
Domains: []string{"example.com"},
},
wantErr: true,
},
{
name: "missing domains",
secret: Secret{
Type: "BasicAuth",
Username: "user",
Password: "pass",
},
wantErr: true,
},
{
name: "basic auth missing username",
secret: Secret{
Type: "BasicAuth",
Domains: []string{"example.com"},
Password: "pass",
},
wantErr: true,
},
{
name: "basic auth missing password",
secret: Secret{
Type: "BasicAuth",
Domains: []string{"example.com"},
Username: "user",
},
wantErr: true,
},
{
name: "bearer auth missing token",
secret: Secret{
Type: "BearerToken",
Domains: []string{"example.com"},
},
wantErr: true,
},
{
name: "header auth missing headers",
secret: Secret{
Type: "Header",
Domains: []string{"example.com"},
},
wantErr: true,
},
{
name: "cookie auth missing cookies",
secret: Secret{
Type: "Cookie",
Domains: []string{"example.com"},
},
wantErr: true,
},
{
name: "query auth missing params",
secret: Secret{
Type: "Query",
Domains: []string{"example.com"},
},
wantErr: true,
},
{
name: "valid domain regex",
secret: Secret{
Type: "BearerToken",
DomainsRegex: []string{".*\\.example\\.com"},
Token: "abc123",
},
wantErr: false,
},
{
name: "invalid domain regex",
secret: Secret{
Type: "BearerToken",
DomainsRegex: []string{"[invalid"},
Token: "abc123",
},
wantErr: true,
},
{
name: "case insensitive type",
secret: Secret{
Type: "basicauth",
Domains: []string{"example.com"},
Username: "user",
Password: "pass",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.secret.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Secret.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetAuthDataFromFile(t *testing.T) {
// Create temp directory for test files
tmpDir := t.TempDir()
yamlContent := `id: test
info:
name: test
static:
- type: BasicAuth
domains:
- example.com
username: user
password: pass
`
jsonContent := `{
"id": "test",
"info": {"name": "test"},
"static": [
{
"type": "BasicAuth",
"domains": ["example.com"],
"username": "user",
"password": "pass"
}
]
}`
tests := []struct {
name string
filename string
content string
wantErr bool
}{
{
name: "yaml file lowercase",
filename: "secrets.yaml",
content: yamlContent,
wantErr: false,
},
{
name: "yml file lowercase",
filename: "secrets.yml",
content: yamlContent,
wantErr: false,
},
{
name: "json file lowercase",
filename: "secrets.json",
content: jsonContent,
wantErr: false,
},
{
name: "yaml file uppercase",
filename: "secrets.YAML",
content: yamlContent,
wantErr: false,
},
{
name: "json file uppercase",
filename: "secrets.JSON",
content: jsonContent,
wantErr: false,
},
{
name: "invalid extension",
filename: "secrets.txt",
content: yamlContent,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create test file
filePath := filepath.Join(tmpDir, tt.filename)
err := os.WriteFile(filePath, []byte(tt.content), 0644)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
_, err = GetAuthDataFromFile(filePath)
if (err != nil) != tt.wantErr {
t.Errorf("GetAuthDataFromFile() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSecretGetStrategy(t *testing.T) {
tests := []struct {
name string
secret Secret
wantType string
wantNil bool
}{
{
name: "basic auth strategy",
secret: Secret{
Type: "BasicAuth",
Username: "user",
Password: "pass",
},
wantType: "*authx.BasicAuthStrategy",
wantNil: false,
},
{
name: "bearer token strategy",
secret: Secret{
Type: "BearerToken",
Token: "abc123",
},
wantType: "*authx.BearerTokenAuthStrategy",
wantNil: false,
},
{
name: "header strategy",
secret: Secret{
Type: "Header",
Headers: []KV{{Key: "X-API-Key", Value: "secret"}},
},
wantType: "*authx.HeadersAuthStrategy",
wantNil: false,
},
{
name: "cookie strategy",
secret: Secret{
Type: "Cookie",
Cookies: []Cookie{{Key: "session", Value: "abc123"}},
},
wantType: "*authx.CookiesAuthStrategy",
wantNil: false,
},
{
name: "query strategy",
secret: Secret{
Type: "Query",
Params: []KV{{Key: "api_key", Value: "secret"}},
},
wantType: "*authx.QueryAuthStrategy",
wantNil: false,
},
{
name: "unknown type returns nil",
secret: Secret{
Type: "UnknownType",
},
wantNil: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strategy := tt.secret.GetStrategy()
if tt.wantNil {
if strategy != nil {
t.Errorf("GetStrategy() = %T, want nil", strategy)
}
} else {
if strategy == nil {
t.Errorf("GetStrategy() = nil, want %s", tt.wantType)
}
}
})
}
}
+215
View File
@@ -0,0 +1,215 @@
package authx
import (
"net/http"
"testing"
"github.com/projectdiscovery/retryablehttp-go"
)
func TestBasicAuthStrategy(t *testing.T) {
secret := &Secret{
Username: "user",
Password: "pass",
}
strategy := NewBasicAuthStrategy(secret)
t.Run("Apply", func(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
strategy.Apply(req)
user, pass, ok := req.BasicAuth()
if !ok {
t.Error("Basic auth not set")
}
if user != "user" {
t.Errorf("Username = %v, want user", user)
}
if pass != "pass" {
t.Errorf("Password = %v, want pass", pass)
}
})
t.Run("ApplyOnRR", func(t *testing.T) {
req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil)
strategy.ApplyOnRR(req)
user, pass, ok := req.BasicAuth()
if !ok {
t.Error("Basic auth not set")
}
if user != "user" {
t.Errorf("Username = %v, want user", user)
}
if pass != "pass" {
t.Errorf("Password = %v, want pass", pass)
}
})
}
func TestBearerTokenAuthStrategy(t *testing.T) {
secret := &Secret{
Token: "mytoken123",
}
strategy := NewBearerTokenAuthStrategy(secret)
t.Run("Apply", func(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
strategy.Apply(req)
auth := req.Header.Get("Authorization")
expected := "Bearer mytoken123"
if auth != expected {
t.Errorf("Authorization = %v, want %v", auth, expected)
}
})
t.Run("ApplyOnRR", func(t *testing.T) {
req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil)
strategy.ApplyOnRR(req)
auth := req.Header.Get("Authorization")
expected := "Bearer mytoken123"
if auth != expected {
t.Errorf("Authorization = %v, want %v", auth, expected)
}
})
}
func TestHeadersAuthStrategy(t *testing.T) {
// Headers strategy preserves exact casing, so use exact key names
secret := &Secret{
Headers: []KV{
{Key: "X-API-Key", Value: "secret123"},
{Key: "X-Custom", Value: "value"},
},
}
strategy := NewHeadersAuthStrategy(secret)
t.Run("Apply", func(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
strategy.Apply(req)
// Use direct map access since headers preserve exact casing
if got := req.Header["X-API-Key"]; len(got) == 0 || got[0] != "secret123" {
t.Errorf("X-API-Key = %v, want [secret123]", got)
}
if got := req.Header["X-Custom"]; len(got) == 0 || got[0] != "value" {
t.Errorf("X-Custom = %v, want [value]", got)
}
})
t.Run("ApplyOnRR", func(t *testing.T) {
req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil)
strategy.ApplyOnRR(req)
// Use direct map access since headers preserve exact casing
if got := req.Header["X-API-Key"]; len(got) == 0 || got[0] != "secret123" {
t.Errorf("X-API-Key = %v, want [secret123]", got)
}
if got := req.Header["X-Custom"]; len(got) == 0 || got[0] != "value" {
t.Errorf("X-Custom = %v, want [value]", got)
}
})
}
func TestCookiesAuthStrategy(t *testing.T) {
secret := &Secret{
Cookies: []Cookie{
{Key: "session", Value: "abc123"},
{Key: "auth", Value: "xyz789"},
},
}
strategy := NewCookiesAuthStrategy(secret)
t.Run("Apply", func(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
strategy.Apply(req)
cookies := req.Cookies()
if len(cookies) != 2 {
t.Errorf("Expected 2 cookies, got %d", len(cookies))
}
found := make(map[string]string)
for _, c := range cookies {
found[c.Name] = c.Value
}
if found["session"] != "abc123" {
t.Errorf("session cookie = %v, want abc123", found["session"])
}
if found["auth"] != "xyz789" {
t.Errorf("auth cookie = %v, want xyz789", found["auth"])
}
})
t.Run("ApplyOnRR replaces existing cookies", func(t *testing.T) {
req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil)
// Add existing cookie that should be replaced
req.AddCookie(&http.Cookie{Name: "session", Value: "old_value"})
// Add existing cookie that should be kept
req.AddCookie(&http.Cookie{Name: "other", Value: "keep_me"})
strategy.ApplyOnRR(req)
cookies := req.Cookies()
found := make(map[string]string)
for _, c := range cookies {
found[c.Name] = c.Value
}
// New cookie values should override old ones
if found["session"] != "abc123" {
t.Errorf("session cookie = %v, want abc123", found["session"])
}
if found["auth"] != "xyz789" {
t.Errorf("auth cookie = %v, want xyz789", found["auth"])
}
// Existing non-replaced cookie should be preserved
if found["other"] != "keep_me" {
t.Errorf("other cookie = %v, want keep_me", found["other"])
}
})
}
func TestQueryAuthStrategy(t *testing.T) {
secret := &Secret{
Params: []KV{
{Key: "api_key", Value: "secret123"},
{Key: "token", Value: "abc"},
},
}
strategy := NewQueryAuthStrategy(secret)
t.Run("Apply", func(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/path?existing=value", nil)
strategy.Apply(req)
query := req.URL.Query()
if got := query.Get("api_key"); got != "secret123" {
t.Errorf("api_key = %v, want secret123", got)
}
if got := query.Get("token"); got != "abc" {
t.Errorf("token = %v, want abc", got)
}
if got := query.Get("existing"); got != "value" {
t.Errorf("existing = %v, want value", got)
}
})
t.Run("ApplyOnRR", func(t *testing.T) {
req, _ := retryablehttp.NewRequest("GET", "http://example.com/path?existing=value", nil)
strategy.ApplyOnRR(req)
query := req.Request.URL.Query()
if got := query.Get("api_key"); got != "secret123" {
t.Errorf("api_key = %v, want secret123", got)
}
if got := query.Get("token"); got != "abc" {
t.Errorf("token = %v, want abc", got)
}
if got := query.Get("existing"); got != "value" {
t.Errorf("existing = %v, want value", got)
}
})
}
+256
View File
@@ -0,0 +1,256 @@
package authprovider
import (
"net/url"
"os"
"path/filepath"
"testing"
urlutil "github.com/projectdiscovery/utils/url"
)
func createTestSecretsFile(t *testing.T, content string) string {
t.Helper()
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "secrets.yaml")
err := os.WriteFile(filePath, []byte(content), 0644)
if err != nil {
t.Fatalf("Failed to create test secrets file: %v", err)
}
return filePath
}
func TestFileAuthProviderLookupAddr(t *testing.T) {
content := `id: test
info:
name: test
static:
- type: BasicAuth
domains:
- example.com
- api.example.com:443
username: user
password: pass
- type: BearerToken
domains-regex:
- ".*\\.test\\.com"
token: regextoken
`
filePath := createTestSecretsFile(t, content)
provider, err := NewFileAuthProvider(filePath)
if err != nil {
t.Fatalf("NewFileAuthProvider() error = %v", err)
}
tests := []struct {
name string
addr string
wantCount int
}{
{
name: "exact match",
addr: "example.com",
wantCount: 1,
},
{
name: "exact match case insensitive",
addr: "EXAMPLE.COM",
wantCount: 1,
},
{
name: "with port 443 normalized",
addr: "example.com:443",
wantCount: 1,
},
{
name: "with port 80 normalized",
addr: "example.com:80",
wantCount: 1,
},
{
name: "subdomain exact match",
addr: "api.example.com",
wantCount: 1,
},
{
name: "regex match",
addr: "foo.test.com",
wantCount: 1,
},
{
name: "regex match subdomain",
addr: "bar.baz.test.com",
wantCount: 1,
},
{
name: "no match",
addr: "unknown.com",
wantCount: 0,
},
{
name: "non-standard port not normalized",
addr: "example.com:8080",
wantCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strategies := provider.LookupAddr(tt.addr)
if len(strategies) != tt.wantCount {
t.Errorf("LookupAddr(%q) returned %d strategies, want %d", tt.addr, len(strategies), tt.wantCount)
}
})
}
}
func TestFileAuthProviderLookupURL(t *testing.T) {
content := `id: test
info:
name: test
static:
- type: BasicAuth
domains:
- example.com
username: user
password: pass
`
filePath := createTestSecretsFile(t, content)
provider, err := NewFileAuthProvider(filePath)
if err != nil {
t.Fatalf("NewFileAuthProvider() error = %v", err)
}
t.Run("LookupURL", func(t *testing.T) {
u, _ := url.Parse("https://example.com/path")
strategies := provider.LookupURL(u)
if len(strategies) != 1 {
t.Errorf("LookupURL() returned %d strategies, want 1", len(strategies))
}
})
t.Run("LookupURLX", func(t *testing.T) {
u, _ := urlutil.Parse("https://example.com/path")
strategies := provider.LookupURLX(u)
if len(strategies) != 1 {
t.Errorf("LookupURLX() returned %d strategies, want 1", len(strategies))
}
})
}
func TestMultiAuthProvider(t *testing.T) {
content1 := `id: test1
info:
name: test1
static:
- type: BasicAuth
domains:
- first.com
username: user1
password: pass1
`
content2 := `id: test2
info:
name: test2
static:
- type: BearerToken
domains:
- second.com
token: token2
`
filePath1 := createTestSecretsFile(t, content1)
provider1, err := NewFileAuthProvider(filePath1)
if err != nil {
t.Fatalf("NewFileAuthProvider() error = %v", err)
}
// Create second file in different temp dir
tmpDir2 := t.TempDir()
filePath2 := filepath.Join(tmpDir2, "secrets2.yaml")
err = os.WriteFile(filePath2, []byte(content2), 0644)
if err != nil {
t.Fatalf("Failed to create test secrets file: %v", err)
}
provider2, err := NewFileAuthProvider(filePath2)
if err != nil {
t.Fatalf("NewFileAuthProvider() error = %v", err)
}
multi := NewMultiAuthProvider(provider1, provider2)
tests := []struct {
name string
addr string
wantCount int
}{
{
name: "match first provider",
addr: "first.com",
wantCount: 1,
},
{
name: "match second provider",
addr: "second.com",
wantCount: 1,
},
{
name: "no match",
addr: "third.com",
wantCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strategies := multi.LookupAddr(tt.addr)
if len(strategies) != tt.wantCount {
t.Errorf("LookupAddr(%q) returned %d strategies, want %d", tt.addr, len(strategies), tt.wantCount)
}
})
}
}
func TestNewFileAuthProviderErrors(t *testing.T) {
tests := []struct {
name string
content string
wantErr bool
}{
{
name: "empty secrets",
content: `id: test`,
wantErr: true,
},
{
name: "invalid secret type",
content: `id: test
static:
- type: InvalidType
domains:
- example.com
`,
wantErr: true,
},
{
name: "missing required field",
content: `id: test
static:
- type: BasicAuth
domains:
- example.com
username: user
`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filePath := createTestSecretsFile(t, tt.content)
_, err := NewFileAuthProvider(filePath)
if (err != nil) != tt.wantErr {
t.Errorf("NewFileAuthProvider() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}