Replace the atomic counter-based request ID generation with random int64 values using math/rand/v2. This improves unpredictability and avoids potential ID collisions across client instances or restarts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
5.4 KiB
Go
211 lines
5.4 KiB
Go
package kanboard
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsSameHost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
urlA string
|
|
urlB string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "same host and scheme",
|
|
urlA: "https://example.com/path",
|
|
urlB: "https://example.com/other",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "same host different scheme",
|
|
urlA: "http://example.com/path",
|
|
urlB: "https://example.com/path",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "http with explicit port 80",
|
|
urlA: "http://example.com:80/path",
|
|
urlB: "http://example.com/path",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "https with explicit port 443",
|
|
urlA: "https://example.com:443/path",
|
|
urlB: "https://example.com/path",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "different hosts",
|
|
urlA: "https://example.com/path",
|
|
urlB: "https://other.com/path",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "different non-standard ports",
|
|
urlA: "https://example.com:8080/path",
|
|
urlB: "https://example.com:9090/path",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "non-standard port vs no port",
|
|
urlA: "https://example.com:8080/path",
|
|
urlB: "https://example.com/path",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "case insensitive host",
|
|
urlA: "https://Example.COM/path",
|
|
urlB: "https://example.com/path",
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a, _ := url.Parse(tt.urlA)
|
|
b, _ := url.Parse(tt.urlB)
|
|
result := isSameHost(a, b)
|
|
if result != tt.expected {
|
|
t.Errorf("isSameHost(%q, %q) = %v, want %v", tt.urlA, tt.urlB, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedirectPreservesAuthForSameHost(t *testing.T) {
|
|
redirectCount := 0
|
|
var receivedAuth string
|
|
|
|
// Server that redirects once, then returns success
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
receivedAuth = r.Header.Get("Authorization")
|
|
|
|
if redirectCount == 0 {
|
|
redirectCount++
|
|
// Redirect to same host with different path
|
|
http.Redirect(w, r, "/redirected", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"test"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
|
|
|
// Override endpoint to point to test server
|
|
client.endpoint = server.URL + "/jsonrpc.php"
|
|
|
|
var result string
|
|
err := client.call(t.Context(), "test", nil, &result)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if receivedAuth == "" {
|
|
t.Error("Authorization header was not preserved after redirect")
|
|
}
|
|
}
|
|
|
|
func TestRedirectPreservesCustomAuthHeader(t *testing.T) {
|
|
redirectCount := 0
|
|
var receivedAuth string
|
|
customHeader := "X-Custom-Auth"
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
receivedAuth = r.Header.Get(customHeader)
|
|
|
|
if redirectCount == 0 {
|
|
redirectCount++
|
|
http.Redirect(w, r, "/redirected", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"test"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL).
|
|
WithAuthHeader(customHeader).
|
|
WithAPIToken("test-token")
|
|
|
|
client.endpoint = server.URL + "/jsonrpc.php"
|
|
|
|
var result string
|
|
err := client.call(t.Context(), "test", nil, &result)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if receivedAuth == "" {
|
|
t.Errorf("Custom auth header %q was not preserved after redirect", customHeader)
|
|
}
|
|
}
|
|
|
|
// Note: Cross-domain redirect behavior is handled by Go's http.Client.
|
|
// Go preserves Authorization headers for same-domain redirects (including localhost:port1 to localhost:port2).
|
|
// Our custom redirect handler adds value for custom auth headers (e.g., "X-Custom-Auth")
|
|
// which Go's default behavior doesn't handle.
|
|
|
|
func TestRedirectLimit(t *testing.T) {
|
|
redirectCount := 0
|
|
|
|
// Server that always redirects
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
redirectCount++
|
|
http.Redirect(w, r, "/redirect"+string(rune('0'+redirectCount)), http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
|
client.endpoint = server.URL + "/jsonrpc.php"
|
|
|
|
var result string
|
|
err := client.call(t.Context(), "test", nil, &result)
|
|
|
|
if err == nil {
|
|
t.Fatal("expected error for too many redirects")
|
|
}
|
|
|
|
if !errors.Is(err, ErrTooManyRedirects) {
|
|
t.Errorf("expected ErrTooManyRedirects, got: %v", err)
|
|
}
|
|
|
|
if redirectCount > maxRedirects+1 {
|
|
t.Errorf("followed %d redirects, expected max %d", redirectCount, maxRedirects)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHost(t *testing.T) {
|
|
tests := []struct {
|
|
rawURL string
|
|
expected string
|
|
}{
|
|
{"http://example.com", "example.com"},
|
|
{"http://example.com:80", "example.com"},
|
|
{"http://example.com:8080", "example.com:8080"},
|
|
{"https://example.com", "example.com"},
|
|
{"https://example.com:443", "example.com"},
|
|
{"https://example.com:8443", "example.com:8443"},
|
|
{"https://Example.COM:443", "example.com"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.rawURL, func(t *testing.T) {
|
|
u, _ := url.Parse(tt.rawURL)
|
|
result := normalizeHost(u)
|
|
if result != tt.expected {
|
|
t.Errorf("normalizeHost(%q) = %q, want %q", tt.rawURL, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|