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>
This commit is contained in:
parent
735b288504
commit
f8daa20ddd
9 changed files with 402 additions and 26 deletions
211
redirect_test.go
Normal file
211
redirect_test.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue