diff --git a/client.go b/client.go index 077a91d..e463aa1 100644 --- a/client.go +++ b/client.go @@ -47,6 +47,8 @@ type Client struct { Apps *AppsEndpoint // Fields provides access to the fields within an Onspring instance. Fields *FieldsEndpoint + // Lists provides access to the lists within an Onspring instance. + Lists *ListsEndpoint } // NewClient creates a new Onspring API client with the provided API key. @@ -82,6 +84,7 @@ func NewClient(apiKey string, opts ...ClientOption) *Client { c.Ping = &PingEndpoint{client: c} c.Apps = &AppsEndpoint{client: c} c.Fields = &FieldsEndpoint{client: c} + c.Lists = &ListsEndpoint{client: c} return c } diff --git a/lists.go b/lists.go new file mode 100644 index 0000000..0362534 --- /dev/null +++ b/lists.go @@ -0,0 +1,78 @@ +package onspring + +import ( + "context" + "fmt" + "net/http" +) + +const ( + listsPath = "/lists" +) + +// ListsEndpoint provides access to lists in an Onspring instance. +type ListsEndpoint struct { + client *Client +} + +// SaveListItemRequest represents a request to save a list item. +type SaveListItemRequest struct { + Id string `json:"id,omitempty"` + Name string `json:"name"` + NumericValue *float64 `json:"numericValue,omitempty"` + Color *string `json:"color,omitempty"` +} + +// SaveListItemResponse represents the response for saving a list item. +type SaveListItemResponse struct { + Id string `json:"id"` +} + +// Save creates or updates a list item in the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - listId: The id of the list to save the item to +// - item: The list item to save +// +// Returns: +// - SaveListItemResponse: The response containing the saved list item's id +// - error: An error if the request fails +func (l *ListsEndpoint) Save(ctx context.Context, listId int, item SaveListItemRequest) (SaveListItemResponse, error) { + path := fmt.Sprintf("%s/id/%d/items", listsPath, listId) + req, requestCreationErr := l.client.newRequest(ctx, http.MethodPut, path, nil, item) + + var response SaveListItemResponse + + if requestCreationErr != nil { + return response, requestCreationErr + } + + responseErr := l.client.doWithJsonResponse(req, &response) + + if responseErr != nil { + return response, responseErr + } + + return response, nil +} + +// Delete removes a list item from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - listId: The id of the list +// - itemId: The id of the list item to delete +// +// Returns: +// - error: An error if the request fails +func (l *ListsEndpoint) Delete(ctx context.Context, listId int, itemId string) error { + path := fmt.Sprintf("%s/id/%d/itemId/%s", listsPath, listId, itemId) + req, requestCreationErr := l.client.newRequest(ctx, http.MethodDelete, path, nil, nil) + + if requestCreationErr != nil { + return requestCreationErr + } + + return l.client.do(req) +} diff --git a/lists_test.go b/lists_test.go new file mode 100644 index 0000000..3fbc6ed --- /dev/null +++ b/lists_test.go @@ -0,0 +1,266 @@ +package onspring_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestLists(t *testing.T) { + t.Run("Save", func(t *testing.T) { + t.Run("it should return an error if context is nil", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + var nilContext context.Context = nil + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + } + + _, err := client.Lists.Save(nilContext, 1, listItem) + + if err == nil { + t.Errorf("Expected error for nil context, got nil") + } + }) + + t.Run("it should return an error if context is canceled", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ctx, cancel := context.WithCancel(t.Context()) + + cancel() + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + } + + _, err := client.Lists.Save(ctx, 1, listItem) + + if err == nil { + t.Errorf("Expected error for canceled context, got nil") + } + }) + + t.Run("it should return an error if encounters a network error", func(t *testing.T) { + client := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://invalid-url"), + onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), + ) + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + } + + _, err := client.Lists.Save(t.Context(), 1, listItem) + + if err == nil { + t.Errorf("Expected network error, got nil") + } + }) + + t.Run("it should return an error if create a request fails", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + invalidClient := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://[::1]:namedport"), + onspring.WithHTTPClient(client.HTTPClient()), + ) + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + } + + _, err := invalidClient.Lists.Save(t.Context(), 1, listItem) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the endpoint returns a non-2xx status code", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + } + + _, err := client.Lists.Save(t.Context(), 1, listItem) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a PUT request to the /lists/id/:listId/items endpoint and return a response when successful", func(t *testing.T) { + listId := 1 + numericValue := 0.0 + color := "#ffffff" + + listItem := onspring.SaveListItemRequest{ + Name: "List Value", + NumericValue: &numericValue, + Color: &color, + } + + expectedResponse := onspring.SaveListItemResponse{ + Id: "d4a3c2b1-e5f6-7890-abcd-ef1234567890", + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("Expected PUT method, got %s", r.Method) + } + + expectedPath := fmt.Sprintf("/lists/id/%d/items", listId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + var body onspring.SaveListItemRequest + err := json.NewDecoder(r.Body).Decode(&body) + + if err != nil { + t.Errorf("Expected to decode request body, but got error: %v", err) + } + + if !reflect.DeepEqual(listItem, body) { + t.Errorf("Expected body to be %v but got %v", listItem, body) + } + + jsonData, _ := json.Marshal(expectedResponse) + + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + response, err := client.Lists.Save(t.Context(), listId, listItem) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !reflect.DeepEqual(expectedResponse, response) { + t.Errorf("Expected %v but got %v", expectedResponse, response) + } + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("it should return an error if context is nil", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + var nilContext context.Context = nil + + err := client.Lists.Delete(nilContext, 1, "d4a3c2b1-e5f6-7890-abcd-ef1234567890") + + if err == nil { + t.Errorf("Expected error for nil context, got nil") + } + }) + + t.Run("it should return an error if context is canceled", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + ctx, cancel := context.WithCancel(t.Context()) + + cancel() + + err := client.Lists.Delete(ctx, 1, "d4a3c2b1-e5f6-7890-abcd-ef1234567890") + + if err == nil { + t.Errorf("Expected error for canceled context, got nil") + } + }) + + t.Run("it should return an error if encounters a network error", func(t *testing.T) { + client := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://invalid-url"), + onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), + ) + + err := client.Lists.Delete(t.Context(), 1, "d4a3c2b1-e5f6-7890-abcd-ef1234567890") + + if err == nil { + t.Errorf("Expected network error, got nil") + } + }) + + t.Run("it should return an error if create a request fails", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + invalidClient := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://[::1]:namedport"), + onspring.WithHTTPClient(client.HTTPClient()), + ) + + err := invalidClient.Lists.Delete(t.Context(), 1, "d4a3c2b1-e5f6-7890-abcd-ef1234567890") + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the endpoint returns a non-2xx status code", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + err := client.Lists.Delete(t.Context(), 1, "d4a3c2b1-e5f6-7890-abcd-ef1234567890") + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a DELETE request to the /lists/id/:listId/itemId/:itemId endpoint and return no error if receives 204 status code", func(t *testing.T) { + listId := 1 + itemId := "d4a3c2b1-e5f6-7890-abcd-ef1234567890" + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("Expected DELETE method, got %s", r.Method) + } + + expectedPath := fmt.Sprintf("/lists/id/%d/itemId/%s", listId, itemId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + w.WriteHeader(http.StatusNoContent) + }) + + err := client.Lists.Delete(t.Context(), listId, itemId) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + }) +}