Validate and compose provider endpoints
This commit is contained in:
39
internal/domain/endpoint.go
Normal file
39
internal/domain/endpoint.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NormalizeOpenAICompatibleBaseEndpoint trims and validates a source-neutral
|
||||
// OpenAI-compatible provider base endpoint.
|
||||
func NormalizeOpenAICompatibleBaseEndpoint(endpoint string) (string, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return "", errors.New("endpoint must not be blank")
|
||||
}
|
||||
if strings.Contains(endpoint, "#") {
|
||||
return "", errors.New("endpoint must not contain a fragment")
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", errors.New("endpoint must be a valid URL")
|
||||
}
|
||||
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", errors.New("endpoint must use http or https")
|
||||
}
|
||||
if !parsed.IsAbs() || parsed.Hostname() == "" {
|
||||
return "", errors.New("endpoint must be absolute and include a host")
|
||||
}
|
||||
if parsed.User != nil {
|
||||
return "", errors.New("endpoint must not contain user information")
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.ForceQuery {
|
||||
return "", errors.New("endpoint must not contain a query string")
|
||||
}
|
||||
|
||||
return parsed.String(), nil
|
||||
}
|
||||
47
internal/domain/endpoint_test.go
Normal file
47
internal/domain/endpoint_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeOpenAICompatibleBaseEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "http host", endpoint: "http://provider.example", want: "http://provider.example"},
|
||||
{name: "https nested path and whitespace", endpoint: " HTTPS://provider.example/api/openai/v1 ", want: "https://provider.example/api/openai/v1"},
|
||||
{name: "IPv4 host and port", endpoint: "http://127.0.0.1:8080/v1", want: "http://127.0.0.1:8080/v1"},
|
||||
{name: "IPv6 host and port", endpoint: "https://[::1]:8443/v1", want: "https://[::1]:8443/v1"},
|
||||
{name: "repeated trailing slashes", endpoint: "https://provider.example/v1///", want: "https://provider.example/v1///"},
|
||||
{name: "blank", endpoint: " \t\n ", wantErr: true},
|
||||
{name: "relative path", endpoint: "/api/v1", wantErr: true},
|
||||
{name: "scheme relative", endpoint: "//provider.example/v1", wantErr: true},
|
||||
{name: "missing host", endpoint: "https:///v1", wantErr: true},
|
||||
{name: "unsupported scheme", endpoint: "ftp://provider.example/v1", wantErr: true},
|
||||
{name: "user information", endpoint: "https://user:secret@provider.example/v1", wantErr: true},
|
||||
{name: "query", endpoint: "https://provider.example/v1?mode=chat", wantErr: true},
|
||||
{name: "empty query", endpoint: "https://provider.example/v1?", wantErr: true},
|
||||
{name: "fragment", endpoint: "https://provider.example/v1#chat", wantErr: true},
|
||||
{name: "empty fragment", endpoint: "https://provider.example/v1#", wantErr: true},
|
||||
{name: "malformed URL", endpoint: "https://provider.example/%zz", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := NormalizeOpenAICompatibleBaseEndpoint(tc.endpoint)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected endpoint error, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("normalize endpoint: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("normalized endpoint = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user