diff --git a/README.md b/README.md index 4a20556..c0706b8 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,273 @@ -# `functions-go` +# Go Supabase Functions Client -Golang client library to interact with Supabase Functions. +A Go client library for interacting with Supabase Edge Functions. This library provides a convenient way to invoke your serverless functions, supporting both POST and GET requests, JSON payloads, and query parameters. -## Quick start +## Features -### Installation +* Initialize client with Supabase function URL and service token. +* Invoke functions using POST with JSON payloads. +* Invoke functions using GET with optional query parameters. +* Handles JSON request and response bodies. +* Returns raw `[]byte` for response body, allowing flexible unmarshaling. -Install the package using: +## Installation -```shell +To use this library in your Go project, you can get it using: + +```bash go get github.com/supabase-community/functions-go ``` -### Usage +## Usage + +### 1. Initialize the Client -The following example demonstrates how to create a client, marshal data into JSON, and make a request to execute a function on the server. +First, create a new client instance using your Supabase Function's base URL and your service (or anon) key. Refer to your `client.go` for the exact `NewClient` signature and usage. + +```go +import ( + "log" + "fmt" + "github.com/supabase-community/functions-go" +) + +func main() { + // Replace with your actual Supabase Functions URL and token + supabaseURL := "https://.functions.supabase.co" + supabaseToken := "" + + // Example: + client := functions.NewClient(supabaseURL, supabaseToken, nil) + // We'll assume 'client' is an instance of *functions.Client for the examples below. + // Please adapt this initialization according to your NewClient function in client.go +} +``` +*(Note: The examples below assume `client` is a properly initialized `*functions.Client` instance based on your `client.go`.)* + +### 2. Invoking Functions + +The `Invoke` method is used to call your Supabase Edge Functions. + +`Invoke(functionName string, method string, payload interface{}) ([]byte, error)` + +* `functionName`: The name of the Supabase function to call (e.g., "hello-world"). +* `method`: The HTTP method, either "POST" or "GET" (case-insensitive). +* `payload`: + * For "POST" requests: The data to be marshaled into a JSON body (e.g., a `map[string]interface{}` or a struct). + * For "GET" requests: + * If `nil`, no query parameters are sent. + * If a `map[string]string` or `map[string]interface{}`, these are converted to URL query parameters. +* Returns: + * `[]byte`: The raw response body from the function. + * `error`: An error if the invocation failed. + +#### Example: POST Request ```go package main import ( - "fmt" - "log" + "encoding/json" + "log" + "fmt" + "github.com/supabase-community/functions-go" +) + +// Assume 'client' is an initialized *functions.Client from your NewClient function +// var client *functions.Client + +type PostPayload struct { + Name string `json:"name"` + Age int `json:"age"` +} + +type PostResponse struct { + Message string `json:"message"` + User string `json:"user"` +} + +func main() { + // Initialize client here based on your client.go ... + supabaseURL := "https://.functions.supabase.co" + supabaseToken := "" + client = functions.NewClient(supabaseURL, supabaseToken, nil) + if client == nil || client.clientError != nil { // Adjust error checking as per NewClient + log.Fatalf("Failed to create client.") + } - "github.com/supabase-community/functions-go" + functionName := "user-profile" + payloadData := PostPayload{Name: "Jane Doe", Age: 30} + + // Ensure 'client' is initialized before calling Invoke + responseBody, err := client.Invoke(functionName, "POST", payloadData) + if err != nil { + log.Fatalf("Failed to invoke function '%s' with POST: %v", functionName, err) + } + + log.Printf("Raw POST response for '%s': %s", functionName, string(responseBody)) + + var parsedResponse PostResponse + err = json.Unmarshal(responseBody, &parsedResponse) + if err != nil { + log.Fatalf("Failed to unmarshal POST response for '%s': %v", functionName, err) + } + + fmt.Printf("Successfully called '%s'. Message: %s, User: %s", functionName, parsedResponse.Message, parsedResponse.User) +} +``` +*(Note: The actual client initialization and Invoke call in the example above are commented out. You'll need to ensure `client` is properly initialized using your `NewClient` function before these snippets can run.)* + +#### Example: GET Request without Query Parameters + +```go +package main + +import ( + "encoding/json" + "log" + "fmt" + // "your/module/path/functions" ) +// Assume 'client' is an initialized *functions.Client +// var client *functions.Client + +type GetItemResponse struct { + ItemID string `json:"itemId"` + ItemName string `json:"itemName"` +} + func main() { - client := functions.NewClient("https://abc.supabase.co/functions/v1", "", nil) + // Initialize client here based on your client.go ... + + functionName := "get-item" + + responseBody, err := client.Invoke(functionName, "GET", nil) + if err != nil { + log.Fatalf("Failed to invoke function '%s' with GET: %v", functionName, err) + } - // Define your data struct - type Post struct { - Title string `json:"title"` - Content string `json:"content"` - } - post := Post{Title: "Hello, world!", Content: "This is a new post."} + log.Printf("Raw GET response for '%s': %s", functionName, string(responseBody)) - // Invoke the function with the post data - response, err := client.Invoke("createPost", post) - if err != nil { - log.Fatal(err) - } + var items []GetItemResponse + err = json.Unmarshal(responseBody, &items) + if err != nil { + log.Fatalf("Failed to unmarshal GET response for '%s': %v", functionName, err) + } - fmt.Println("Response from server:", response) + fmt.Printf("Successfully called '%s'. Received %d items.", functionName, len(items)) + if len(items) > 0 { + fmt.Printf("First item: ID=%s, Name=%s", items[0].ItemID, items[0].ItemName) + } } ``` -This code will marshal the `Post` struct into JSON, send it to the `createPost` function, and print the response. +#### Example: GET Request with Query Parameters + +```go +package main + +import ( + "encoding/json" + "log" + "fmt" + // "your/module/path/functions" +) + +// Assume 'client' is an initialized *functions.Client +// var client *functions.Client + +type Product struct { + ID string `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` +} + +func main() { + // Initialize client here based on your client.go ... + + functionName := "search-products" + queryParams := map[string]interface{}{ + "category": "electronics", + "limit": 10, + "inStock": true, + } + + responseBody, err := client.Invoke(functionName, "GET", queryParams) + if err != nil { + log.Fatalf("Failed to invoke function '%s' with GET and query params: %v", functionName, err) + } + + log.Printf("Raw GET response for '%s' with params: %s", functionName, string(responseBody)) + + var products []Product + err = json.Unmarshal(responseBody, &products) + if err != nil { + log.Fatalf("Failed to unmarshal GET response for '%s': %v", functionName, err) + } + + fmt.Printf("Successfully called '%s'. Found %d products.", functionName, len(products)) + // Process products... +} +``` + +### 3. Handling the Response + +The `Invoke` function returns the raw response body as `[]byte`. If your function returns JSON (which is common), you can unmarshal it using the standard `encoding/json` package: + +```go +var result MyExpectedStruct // or map[string]interface{} +err = json.Unmarshal(responseBody, &result) +if err != nil { +// Handle error +} +// Use 'result' +``` + +If your function returns plain text or other non-JSON data, you can convert the `[]byte` to a string: +```go +responseText := string(responseBody) +// Use 'responseText' +``` + +### 4. Error Handling + +The `Invoke` method returns an error if: +* The HTTP method is unsupported (only "GET" and "POST" are allowed). +* The payload for a POST request cannot be marshaled to JSON. +* The payload for a GET request (if provided for query parameters) is not a `map[string]string` or `map[string]interface{}`. +* The HTTP request cannot be created. +* The request execution fails (e.g., network issues). +* Reading the response body fails. +* The server responds with an HTTP status code >= 400. The error message will include the status code and often the response body from the server for easier debugging. + +Always check the returned error. + +## Running Tests + +To run the unit tests for this library (if `functions_test.go` is provided): +```bash +go test -v ./... +``` + +## Contributing + +Details on how to contribute can be added here. For example: + +1. Fork the repository. +2. Create your feature branch (`git checkout -b feature/AmazingFeature`). +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`). +4. Push to the branch (`git push origin feature/AmazingFeature`). +5. Open a Pull Request. ## License -This repository is licensed under the MIT License. +This repository is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. ## Credits +This library is inspired by the Supabase ecosystem and community projects. For further inspiration and a JavaScript-based client, visit: -- [functions-js](https://github.com/supabase/functions-js) +* [functions-js](https://github.com/supabase/functions-js) +* Official Supabase documentation: [https://supabase.com/docs/guides/functions](https://supabase.com/docs/guides/functions) diff --git a/functions.go b/functions.go index ba9b747..b384bbe 100644 --- a/functions.go +++ b/functions.go @@ -6,43 +6,95 @@ import ( "fmt" "io" "net/http" + "net/url" + "strings" ) -func (c *Client) Invoke(functionName string, payload interface{}) (string, error) { - // Marshal the payload to JSON - jsonData, err := json.Marshal(payload) - if err != nil { - return "", err - } +func (c *Client) Invoke(functionName string, method string, payload interface{}) ([]byte, error) { + // Build the base URL + targetURLString := c.clientTransport.baseUrl.String() + "/" + functionName + + var req *http.Request + var err error + + // Normalize method to uppercase for reliable comparison + httpMethod := strings.ToUpper(method) + + switch httpMethod { + case "POST": + jsonData, marshalErr := json.Marshal(payload) + if marshalErr != nil { + return nil, fmt.Errorf("failed to marshal payload for POST request: %w", marshalErr) + } + req, err = http.NewRequest(httpMethod, targetURLString, bytes.NewReader(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create POST request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + case "GET": + parsedURL, parseErr := url.Parse(targetURLString) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse base URL for GET request: %w", parseErr) + } + + if payload != nil { + var queryValues url.Values + switch p := payload.(type) { + case map[string]string: + queryValues = make(url.Values) + for k, v := range p { + queryValues.Set(k, v) + } + case map[string]interface{}: + queryValues = make(url.Values) + for k, v := range p { + queryValues.Set(k, fmt.Sprint(v)) + } + default: + if payload != nil { + return nil, fmt.Errorf("for GET requests, payload must be map[string]string or map[string]interface{} for query parameters, got %T", payload) + } + } + + if len(queryValues) > 0 { + parsedURL.RawQuery = queryValues.Encode() + } + } + + req, err = http.NewRequest(httpMethod, parsedURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create GET request: %w", err) + } - // Build the URL and create the request - url := c.clientTransport.baseUrl.String() + "/" + functionName - req, err := http.NewRequest("POST", url, bytes.NewReader(jsonData)) - if err != nil { - return "", err + default: + return nil, fmt.Errorf("unsupported HTTP method: %s. Only GET and POST are supported", method) } - // Set headers - req.Header.Set("Content-Type", "application/json") + // Set common headers (if any) applicable to all processed methods req.Header.Set("Accept", "application/json") // Execute the request using the client's session - resp, err := c.session.Do(req) - if err != nil { - return "", err + resp, execErr := c.session.Do(req) + if execErr != nil { + return nil, fmt.Errorf("failed to execute request: %w", execErr) } defer resp.Body.Close() // Read the response body - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", err + responseBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, fmt.Errorf("failed to read response body: %w", readErr) } // Check HTTP response status code if resp.StatusCode >= 400 { - return "", fmt.Errorf("server responded with error: %s", resp.Status) + errorMsg := fmt.Sprintf("server responded with error: %s (status code %d)", resp.Status, resp.StatusCode) + if len(responseBody) > 0 { + errorMsg += fmt.Sprintf(", body: %s", string(responseBody)) + } + return nil, fmt.Errorf(errorMsg) } - return string(responseBody), nil + return responseBody, nil } diff --git a/functions_test.go b/functions_test.go new file mode 100644 index 0000000..baebb37 --- /dev/null +++ b/functions_test.go @@ -0,0 +1,487 @@ +package functions + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +// MockRoundTripper is a custom http.RoundTripper for mocking server responses. +type MockRoundTripper func(req *http.Request) (*http.Response, error) + +func (mrt MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return mrt(req) +} + +// NOTE: The Client and transport structs are defined in client.go and are used here. +// Ensure client.go is part of the same 'functions' package. + +// Helper function to create a new client with a mock server +func newTestClient(handler http.HandlerFunc) (*Client, *httptest.Server) { + server := httptest.NewServer(handler) + serverURL, _ := url.Parse(server.URL) // serverURL is *url.URL + + // Create a custom http.Client that uses the test server + mockSessionTransport := MockRoundTripper(func(req *http.Request) (*http.Response, error) { + // Ensure the request URL uses the test server's host and scheme + // The Invoke function will build the full URL using clientTransport.baseUrl + // So, the RoundTripper for the session just needs to execute it. + // However, the httptest.Server expects requests to its specific URL. + // We need to make sure the request passed to DefaultTransport has the test server's URL. + finalReqURL := *serverURL // Dereference to get url.URL, then take parts + finalReqURL.Path = req.URL.Path + finalReqURL.RawQuery = req.URL.RawQuery + req.URL = &finalReqURL + return http.DefaultTransport.RoundTrip(req) + }) + + // The Client's session needs a transport. The mockSessionTransport ensures + // that requests made by the session are correctly routed to the httptest.Server. + + return &Client{ + // clientError can be nil + clientTransport: transport{ // transport.baseUrl is url.URL + header: http.Header{}, + baseUrl: *serverURL, // serverURL from httptest.NewServer is *url.URL + }, + session: http.Client{Transport: mockSessionTransport}, // Client.session is http.Client + }, server +} + +func TestClient_Invoke_POST_Success(t *testing.T) { + expectedResponseString := `{"message": "success"}` + expectedPayload := map[string]string{"key": "value"} + functionName := "testPostFunc" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("Expected POST method, got %s", r.Method) + http.Error(w, "Wrong method", http.StatusMethodNotAllowed) + return + } + // The path check in the handler should be against the functionName only, + // as the test server's base URL is handled by the RoundTripper or client setup. + if r.URL.Path != "/"+functionName { + t.Errorf("Expected path /%s, got %s", functionName, r.URL.Path) + http.Error(w, "Wrong path", http.StatusBadRequest) + return + } + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("Expected Content-Type application/json, got %s", contentType) + http.Error(w, "Wrong content type", http.StatusBadRequest) + return + } + + var receivedPayload map[string]string + err := json.NewDecoder(r.Body).Decode(&receivedPayload) + if err != nil { + t.Errorf("Failed to decode request body: %v", err) + http.Error(w, "Bad request body", http.StatusBadRequest) + return + } + if receivedPayload["key"] != expectedPayload["key"] { + t.Errorf("Expected payload %v, got %v", expectedPayload, receivedPayload) + http.Error(w, "Wrong payload", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "POST", expectedPayload) + if err != nil { + t.Fatalf("Invoke failed: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response %s, got %s", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_GET_Success_NoParams(t *testing.T) { + expectedResponseString := `{"data": "some data"}` + functionName := "testGetFunc" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + http.Error(w, "Wrong method", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/"+functionName { + t.Errorf("Expected path /%s, got %s", functionName, r.URL.Path) + http.Error(w, "Wrong path", http.StatusBadRequest) + return + } + if r.URL.RawQuery != "" { + t.Errorf("Expected no query params, got %s", r.URL.RawQuery) + http.Error(w, "Unexpected query params", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", nil) + if err != nil { + t.Fatalf("Invoke failed: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response %s, got %s", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_GET_Success_WithParams_MapStringString(t *testing.T) { + expectedResponseString := `{"data": "filtered data"}` + functionName := "testGetFiltered" + params := map[string]string{"filter": "active", "limit": "10"} + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + return + } + if r.URL.Path != "/"+functionName { + t.Errorf("Expected path /%s, got %s", functionName, r.URL.Path) + return + } + query := r.URL.Query() + if query.Get("filter") != params["filter"] { + t.Errorf("Expected query param filter=%s, got %s", params["filter"], query.Get("filter")) + } + if query.Get("limit") != params["limit"] { + t.Errorf("Expected query param limit=%s, got %s", params["limit"], query.Get("limit")) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", params) + if err != nil { + t.Fatalf("Invoke failed: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response %s, got %s", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_GET_Success_WithParams_MapStringInterface(t *testing.T) { + expectedResponseString := `{"data": "interface data"}` + functionName := "testGetInterfaceParams" + params := map[string]interface{}{"id": 123, "active": true} + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + return + } + query := r.URL.Query() + if query.Get("id") != "123" { + t.Errorf("Expected query param id=123, got %s", query.Get("id")) + } + if query.Get("active") != "true" { + t.Errorf("Expected query param active=true, got %s", query.Get("active")) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", params) + if err != nil { + t.Fatalf("Invoke failed: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response %s, got %s", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_POST_MarshalError(t *testing.T) { + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + t.Error("Server handler should not be called on marshal error") + http.Error(w, "Should not be reached", http.StatusInternalServerError) + }) + defer server.Close() + + invalidPayload := make(chan int) + _, err := client.Invoke("testFunc", "POST", invalidPayload) + + if err == nil { + t.Fatal("Expected an error from Invoke due to marshaling failure, but got nil") + } + if !strings.Contains(err.Error(), "failed to marshal payload for POST request") { + t.Errorf("Expected marshal error message, got: %v", err) + } +} + +func TestClient_Invoke_GET_InvalidPayloadType(t *testing.T) { + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + t.Error("Server handler should not be called on invalid payload type for GET") + http.Error(w, "Should not be reached", http.StatusInternalServerError) + }) + defer server.Close() + + invalidPayload := []string{"this", "is", "not", "a", "map"} + _, err := client.Invoke("testGetInvalid", "GET", invalidPayload) + + if err == nil { + t.Fatal("Expected an error due to invalid payload type for GET, but got nil") + } + expectedErrorMsg := "for GET requests, payload must be map[string]string or map[string]interface{} for query parameters, got []string" + if !strings.Contains(err.Error(), expectedErrorMsg) { + t.Errorf("Expected error message '%s', got: %v", expectedErrorMsg, err) + } +} + +func TestClient_Invoke_UnsupportedMethod(t *testing.T) { + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + t.Error("Server handler should not be called for unsupported method") + http.Error(w, "Should not be reached", http.StatusInternalServerError) + }) + defer server.Close() + + _, err := client.Invoke("testFunc", "PUT", nil) + if err == nil { + t.Fatal("Expected an error for unsupported HTTP method, but got nil") + } + if !strings.Contains(err.Error(), "unsupported HTTP method: PUT") { + t.Errorf("Expected unsupported method error message, got: %v", err) + } +} + +func TestClient_Invoke_ServerError(t *testing.T) { + errorMessage := "Internal Server Error" + errorStatus := http.StatusInternalServerError + functionName := "testErrorFunc" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(errorStatus) + fmt.Fprint(w, errorMessage) + }) + defer server.Close() + + _, err := client.Invoke(functionName, "GET", nil) + if err == nil { + t.Fatal("Expected an error from Invoke due to server error, but got nil") + } + + expectedErrorSubstring := fmt.Sprintf("server responded with error: %d %s", errorStatus, http.StatusText(errorStatus)) + if !strings.Contains(err.Error(), expectedErrorSubstring) { + t.Errorf("Error message '%v' does not contain expected substring '%s'", err, expectedErrorSubstring) + } + if !strings.Contains(err.Error(), errorMessage) { + t.Errorf("Error message '%v' does not contain the server error body '%s'", err, errorMessage) + } +} + +func TestClient_Invoke_NetworkError(t *testing.T) { + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + }) + server.Close() // Close server immediately to simulate network error + + _, err := client.Invoke("testNetError", "POST", map[string]string{"data": "test"}) + if err == nil { + t.Fatal("Expected a network error, but got nil") + } + if !strings.Contains(err.Error(), "failed to execute request") { + t.Errorf("Expected network error message to contain 'failed to execute request', got: %v", err) + } +} + +type errorReader struct{} + +func (er *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("simulated read error") +} + +func (er *errorReader) Close() error { return nil } + +func TestClient_Invoke_ReadResponseBodyError(t *testing.T) { + functionName := "testReadError" + + // Custom RoundTripper for the session to inject errorReader + mockSessionRT := MockRoundTripper(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: &errorReader{}, + Header: make(http.Header), + }, nil + }) + + serverURL, _ := url.Parse("http://localhost") // This base URL is for client.clientTransport + client := &Client{ + clientTransport: transport{ + header: http.Header{}, + baseUrl: *serverURL, // transport.baseUrl is url.URL + }, + session: http.Client{Transport: mockSessionRT}, // Client.session is http.Client + } + + _, err := client.Invoke(functionName, "GET", nil) + if err == nil { + t.Fatal("Expected an error from reading response body, but got nil") + } + if !strings.Contains(err.Error(), "failed to read response body: simulated read error") { + t.Errorf("Expected read body error message, got: %v", err) + } +} + +func TestClient_Invoke_GET_NilPayload(t *testing.T) { + expectedResponseString := `{"message": "get success no params"}` + functionName := "testGetNilPayload" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + http.Error(w, "Wrong method", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/"+functionName { + t.Errorf("Expected path /%s, got %s", functionName, r.URL.Path) + http.Error(w, "Wrong path", http.StatusBadRequest) + return + } + if r.URL.RawQuery != "" { + t.Errorf("Expected no query params for nil payload, got %s", r.URL.RawQuery) + http.Error(w, "Unexpected query params", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", nil) + if err != nil { + t.Fatalf("Invoke failed for GET with nil payload: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response '%s', got '%s'", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_POST_NilPayload(t *testing.T) { + expectedResponseString := `{"message": "post success with null"}` + functionName := "testPostNilPayload" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("Expected POST method, got %s", r.Method) + http.Error(w, "Wrong method", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/"+functionName { + t.Errorf("Expected path /%s, got %s", functionName, r.URL.Path) + http.Error(w, "Wrong path", http.StatusBadRequest) + return + } + bodyBytes, ioErr := io.ReadAll(r.Body) + if ioErr != nil { + t.Errorf("Failed to read request body: %v", ioErr) + http.Error(w, "Cannot read body", http.StatusBadRequest) + return + } + if string(bodyBytes) != "null" { + t.Errorf("Expected body 'null' for nil payload, got '%s'", string(bodyBytes)) + http.Error(w, "Wrong body", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "POST", nil) + if err != nil { + t.Fatalf("Invoke failed for POST with nil payload: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response '%s', got '%s'", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_GET_EmptyMapPayload(t *testing.T) { + expectedResponseString := `{"message": "get success empty params"}` + functionName := "testGetEmptyMap" + params := map[string]string{} // Empty map + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + return + } + if r.URL.RawQuery != "" { + t.Errorf("Expected no query params for empty map payload, got %s", r.URL.RawQuery) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, expectedResponseString) + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", params) + if err != nil { + t.Fatalf("Invoke failed for GET with empty map payload: %v", err) + } + if string(responseBytes) != expectedResponseString { + t.Errorf("Expected response '%s', got '%s'", expectedResponseString, string(responseBytes)) + } +} + +func TestClient_Invoke_ResponseParsing_JSON(t *testing.T) { + expectedData := map[string]interface{}{ + "id": float64(123), + "name": "Test Item", + "enabled": true, + } + serverResponseBytes, _ := json.Marshal(expectedData) + // serverResponse string is not directly used for comparison, server just sends bytes + functionName := "testJsonResponseFunc" + + client, server := newTestClient(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(serverResponseBytes) // Server sends bytes + }) + defer server.Close() + + responseBytes, err := client.Invoke(functionName, "GET", nil) + if err != nil { + t.Fatalf("Invoke failed: %v", err) + } + + var parsedResponse map[string]interface{} + err = json.Unmarshal(responseBytes, &parsedResponse) // Unmarshal directly from []byte + if err != nil { + t.Fatalf("Failed to unmarshal JSON response: %v. Response string: %s", err, string(responseBytes)) + } + + // Verify the parsed data + if parsedResponse["name"] != expectedData["name"] { + t.Errorf("Expected name '%s', got '%s'", expectedData["name"], parsedResponse["name"]) + } + if parsedResponse["id"].(float64) != expectedData["id"].(float64) { + t.Errorf("Expected id '%v', got '%v'", expectedData["id"], parsedResponse["id"]) + } + if parsedResponse["enabled"] != expectedData["enabled"] { + t.Errorf("Expected enabled '%v', got '%v'", expectedData["enabled"], parsedResponse["enabled"]) + } +} diff --git a/test/integration/functions_test.go b/test/integration/functions_test.go index c0d85db..e846167 100644 --- a/test/integration/functions_test.go +++ b/test/integration/functions_test.go @@ -19,7 +19,7 @@ func TestHello(t *testing.T) { Name string `json:"name"` } b := Body{Name: "world"} - resp, err := client.Invoke("hello", b) + resp, err := client.Invoke("hello", "POST", b) if err != nil { t.Fatalf("Invoke failed: %s", err) } @@ -33,7 +33,7 @@ func TestErrorHandling(t *testing.T) { Name string `json:"name"` } b := Body{Name: "error"} - resp, err := client.Invoke("hello", b) + resp, err := client.Invoke("hello", "POST", b) if err != nil { t.Fatalf("Invoke failed: %s", err) }