Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions internal/client/clienttools.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,30 @@

func buildPath(baseUrl string, path string, parameters map[string]string, query map[string]string) *url.URL {
for key, param := range parameters {
path = strings.Replace(path, fmt.Sprintf("{%s}", key), fmt.Sprintf("%v", param), 1)
param = url.PathEscape(param)
path = strings.Replace(path, fmt.Sprintf("{%s}", key), param, 1)
}

params := url.Values{}

for key, param := range query {
params.Add(key, param)
queryParam := url.QueryEscape(param)
params.Add(key, queryParam)
}

parsed, err := url.Parse(baseUrl)
if err != nil {
return nil
}

parsed.Path = pathutil.Join(parsed.Path, path)
// Remove trailing slash from base path if present
parsed.Opaque = "//" + pathutil.Join(parsed.Host, parsed.Path, path)
parsed.RawQuery = params.Encode()
parsed, err = url.Parse(parsed.String())
if err != nil {
return nil
}

Check warning on line 72 in internal/client/clienttools.go

View check run for this annotation

Codecov / codecov/patch

internal/client/clienttools.go#L71-L72

Added lines #L71 - L72 were not covered by tests
return parsed
}

func getValidResponseCode(codes *orderedmap.Map[string, *v3.Response]) ([]int, error) {
var validCodes []int
for code := codes.First(); code != nil; code = code.Next() {
Expand Down
152 changes: 152 additions & 0 deletions internal/client/clienttools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package restclient

import (
"fmt"
"net/url"
"reflect"
"testing"
)

func TestBuildPath_Basic(t *testing.T) {
baseUrl := "https://api.example.com/v1"
path := "/users/{userId}/posts/{postId}"
parameters := map[string]string{
"userId": "42",
"postId": "99",
}
query := map[string]string{
"sort": "desc",
"page": "2",
}

got := buildPath(baseUrl, path, parameters, query)
if got == nil {
t.Fatalf("buildPath returned nil")
}

got, err := url.Parse(got.String())
if err != nil {
t.Fatalf("failed to parse base URL: %v", err)
}

expectedPath := "/v1/users/42/posts/99"
if got.Path != "/v1/users/42/posts/99" {
t.Errorf("expected path %q, got %q", expectedPath, got.Path)
}

wantQuery := url.Values{"sort": {"desc"}, "page": {"2"}}.Encode()
if got.RawQuery != wantQuery && got.RawQuery != "page=2&sort=desc" {
t.Errorf("expected query %q, got %q", wantQuery, got.RawQuery)
}
}

func TestBuildPath_WithPathParamsWithSlashes(t *testing.T) {
baseUrl := "https://api.example.com/v1"
path := "/users/{userId}/posts/{postId}"
parameters := map[string]string{
"userId": "42/123",
"postId": "99",
}
query := map[string]string{}

got := buildPath(baseUrl, path, parameters, query)
if got == nil {
t.Fatalf("buildPath returned nil")
}

fmt.Println("Got Path:", got.RawQuery)

wantPath := baseUrl + "/users/42%2F123/posts/99"
if got.String() != wantPath {
t.Errorf("expected path %q, got %q", wantPath, got.String())
}
}

func TestBuildPath_NoParams(t *testing.T) {
baseUrl := "https://api.example.com"
path := "/status"
parameters := map[string]string{}
query := map[string]string{}

got := buildPath(baseUrl, path, parameters, query)
if got == nil {
t.Fatalf("buildPath returned nil")
}
got, err := url.Parse(got.String())
if err != nil {
t.Fatalf("failed to parse base URL: %v", err)
}

wantPath := "/status"
if got.Path != wantPath {
t.Errorf("expected path %q, got %q", wantPath, got.Path)
}
if got.RawQuery != "" {
t.Errorf("expected empty query, got %q", got.RawQuery)
}
}

func TestBuildPath_InvalidBaseURL(t *testing.T) {
baseUrl := "://bad-url"
path := "/foo"
parameters := map[string]string{}
query := map[string]string{}

got := buildPath(baseUrl, path, parameters, query)
if got != nil {
t.Errorf("expected nil for invalid baseUrl, got %v", got)
}
}

func TestBuildPath_MultipleQueryParams(t *testing.T) {
baseUrl := "https://api.example.com"
path := "/search"
parameters := map[string]string{}
query := map[string]string{
"q": "golang",
"lang": "en",
}

got := buildPath(baseUrl, path, parameters, query)
if got == nil {
t.Fatalf("buildPath returned nil")
}

got, err := url.Parse(got.String())
if err != nil {
t.Fatalf("failed to parse base URL: %v", err)
}

wantPath := "/search"
if got.Path != wantPath {
t.Errorf("expected path %q, got %q", wantPath, got.Path)
}

parsedQuery, _ := url.ParseQuery(got.RawQuery)
expectedQuery := url.Values{"q": {"golang"}, "lang": {"en"}}
if !reflect.DeepEqual(parsedQuery, expectedQuery) {
t.Errorf("expected query %v, got %v", expectedQuery, parsedQuery)
}
}

func TestBuildPath_PathWithNoLeadingSlash(t *testing.T) {
baseUrl := "https://api.example.com/api"
path := "foo/bar"
parameters := map[string]string{}
query := map[string]string{}

got := buildPath(baseUrl, path, parameters, query)
if got == nil {
t.Fatalf("buildPath returned nil")
}

got, err := url.Parse(got.String())
if err != nil {
t.Fatalf("failed to parse base URL: %v", err)
}

wantPath := "/api/foo/bar"
if got.Path != wantPath {
t.Errorf("expected path %q, got %q", wantPath, got.Path)
}
}
20 changes: 20 additions & 0 deletions internal/client/restclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"

Expand Down Expand Up @@ -42,6 +43,25 @@ func TestCallWithRecorder(t *testing.T) {
expected interface{}
expectedError string
}{
{
name: "path with slash in path parameter",
handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
r.URL, _ = url.Parse(r.URL.String())
assert.Equal(t, "/api/test/123%2F456", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{"message": "success"})
},
path: "/api/test/{id}",
opts: &RequestConfiguration{
Method: "GET",
Parameters: map[string]string{
"id": "123%2F456",
},
},
expected: map[string]interface{}{"message": "success"},
},
{
name: "successful GET request",
handler: func(w http.ResponseWriter, r *http.Request) {
Expand Down
24 changes: 24 additions & 0 deletions internal/client/testdata/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ servers:
description: Test server

paths:
/api/test/{id}:
get:
summary: Get test data by ID
operationId: getTestById
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
id:
type: string
name:
type: string
'404':
description: Not found
/api/test:
get:
summary: Get test data
Expand Down
1 change: 1 addition & 0 deletions internal/restResources/restResources.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c
if mg == nil {
return controller.ExternalObservation{}, fmt.Errorf("custom resource is nil")
}

log := h.logger.WithValues("op", "Observe").
WithValues("apiVersion", mg.GetAPIVersion()).
WithValues("kind", mg.GetKind()).
Expand Down
Loading