kanboard-api/redirect_test.go
Oliver Jakoubek f8daa20ddd feat: use random request IDs instead of sequential counter
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>
2026-01-27 10:21:31 +01:00

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)
}
})
}
}