feat: implement field endpoints

This commit is contained in:
Stevan Freeborn
2026-03-25 16:36:53 -05:00
parent 0063830cfb
commit ce85b08fbc
7 changed files with 1324 additions and 20 deletions
+110 -4
View File
@@ -102,6 +102,12 @@ import (
client := onspring.NewClient("your-api-key") client := onspring.NewClient("your-api-key")
app, err := client.Apps.Get(context.TODO(), 1) app, err := client.Apps.Get(context.TODO(), 1)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved App: %+v\n", app)
}
``` ```
#### Get Apps by Page #### Get Apps by Page
@@ -116,7 +122,13 @@ import (
client := onspring.NewClient("your-api-key") client := onspring.NewClient("your-api-key")
page, err := client.Apps.List(t.Context()) page, err := client.Apps.List(context.TODO())
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved Page %d of Apps: %+v\n", page.PageNumber, page.Items)
}
``` ```
##### Retrieve all pages ##### Retrieve all pages
@@ -129,8 +141,12 @@ import (
client := onspring.NewClient("your-api-key") client := onspring.NewClient("your-api-key")
for app, err := range client.Apps.ListAll(t.Context()) { for app, err := range client.Apps.ListAll(context.TODO()) {
// Do stuff if err != nil {
fmt.Printf("Error during iteration: %v\n", err)
break
}
fmt.Printf("Retrieved App: %+v\n", app)
} }
``` ```
@@ -144,5 +160,95 @@ import (
client := onspring.NewClient("your-api-key") client := onspring.NewClient("your-api-key")
batch, err := client.Apps.GetMany(t.Context(), []int{ 1 }) batch, err := client.Apps.GetMany(context.TODO(), []int{ 1, 2 })
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved App Batch (Count: %d): %+v\n", batch.Count, batch.Items)
}
``` ```
### Fields
#### Get Field by Id
```go
import (
"context"
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
field, err := client.Fields.Get(context.TODO(), 1)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved Field: %+v\n", field)
}
```
#### Get Fields by App
##### Retrieve a single page
```go
import (
"context"
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
page, err := client.Fields.List(context.TODO(), 1)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved Page %d of Fields: %+v\n", page.PageNumber, page.Items)
}
```
##### Retrieve all pages
```go
import (
"context"
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
for field, err := range client.Fields.ListAll(context.TODO(), 1) {
if err != nil {
fmt.Printf("Error during iteration: %v\n", err)
break
}
fmt.Printf("Retrieved Field: %+v\n", field)
}
```
#### Get Fields by Batch
```go
import (
"context"
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
batch, err := client.Fields.GetMany(context.TODO(), []int{ 1, 2 })
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Retrieved Field Batch (Count: %d): %+v\n", batch.Count, batch.Items)
}
+1 -14
View File
@@ -92,7 +92,7 @@ func (a *AppsEndpoint) ListAll(ctx context.Context, pagingOpts ...PagingOption)
} }
} }
if page.TotalPages == page.PageNumber { if page.PageNumber >= page.TotalPages {
break break
} }
@@ -156,16 +156,3 @@ func (a *AppsEndpoint) Get(ctx context.Context, id int) (App, error) {
return app, nil return app, nil
} }
func createPagingRequest(pagingOpts []PagingOption) *PagingRequest {
pagingRequest := &PagingRequest{
PageNumber: 1,
PageSize: 50,
}
for _, opt := range pagingOpts {
opt(pagingRequest)
}
return pagingRequest
}
+14 -1
View File
@@ -514,6 +514,8 @@ func TestApps(t *testing.T) {
Items: apps, Items: apps,
} }
expectedIds := []int{apps[0].Id}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("Expected POST method, got %s", r.Method) t.Errorf("Expected POST method, got %s", r.Method)
@@ -523,6 +525,17 @@ func TestApps(t *testing.T) {
t.Errorf("Expected /apps/batch-get endpoint, got %s", r.URL.Path) t.Errorf("Expected /apps/batch-get endpoint, got %s", r.URL.Path)
} }
var ids []int
err := json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
t.Errorf("Expected to decode request body, but got error: %v", err)
}
if !slices.Equal(expectedIds, ids) {
t.Errorf("Expected body to be %v but got %v", expectedIds, ids)
}
jsonData, _ := json.Marshal(expectedBatch) jsonData, _ := json.Marshal(expectedBatch)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -530,7 +543,7 @@ func TestApps(t *testing.T) {
_, _ = w.Write(jsonData) _, _ = w.Write(jsonData)
}) })
batch, _ := client.Apps.GetMany(t.Context(), []int{apps[0].Id}) batch, _ := client.Apps.GetMany(t.Context(), expectedIds)
if !reflect.DeepEqual(expectedBatch, batch) { if !reflect.DeepEqual(expectedBatch, batch) {
t.Errorf("Expected %v but got %v", expectedBatch, batch) t.Errorf("Expected %v but got %v", expectedBatch, batch)
+19 -1
View File
@@ -4,6 +4,7 @@
package onspring package onspring
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -44,6 +45,8 @@ type Client struct {
Ping *PingEndpoint Ping *PingEndpoint
// Apps provides access to the apps within an Onspring instance. // Apps provides access to the apps within an Onspring instance.
Apps *AppsEndpoint Apps *AppsEndpoint
// Fields provides access to the fields within an Onspring instance.
Fields *FieldsEndpoint
} }
// NewClient creates a new Onspring API client with the provided API key. // NewClient creates a new Onspring API client with the provided API key.
@@ -78,6 +81,7 @@ func NewClient(apiKey string, opts ...ClientOption) *Client {
c.Ping = &PingEndpoint{client: c} c.Ping = &PingEndpoint{client: c}
c.Apps = &AppsEndpoint{client: c} c.Apps = &AppsEndpoint{client: c}
c.Fields = &FieldsEndpoint{client: c}
return c return c
} }
@@ -179,7 +183,7 @@ func (c *Client) handleAPIError(resp *http.Response) error {
// Returns: // Returns:
// - *http.Request: The prepared HTTP request // - *http.Request: The prepared HTTP request
// - error: An error if the context is nil or request creation fails // - error: An error if the context is nil or request creation fails
func (c *Client) newRequest(ctx context.Context, method, path string, queryParams map[string]string, _ any) (*http.Request, error) { func (c *Client) newRequest(ctx context.Context, method, path string, queryParams map[string]string, body any) (*http.Request, error) {
if ctx == nil { if ctx == nil {
return nil, fmt.Errorf("context must not be nil") return nil, fmt.Errorf("context must not be nil")
} }
@@ -201,6 +205,16 @@ func (c *Client) newRequest(ctx context.Context, method, path string, queryParam
var bodyReader io.Reader var bodyReader io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewReader(jsonData)
}
req, err := http.NewRequestWithContext(ctx, method, validUrl.String(), bodyReader) req, err := http.NewRequestWithContext(ctx, method, validUrl.String(), bodyReader)
if err != nil { if err != nil {
@@ -210,5 +224,9 @@ func (c *Client) newRequest(ctx context.Context, method, path string, queryParam
req.Header.Set(defaultAPIKeyHeader, c.apiKey) req.Header.Set(defaultAPIKeyHeader, c.apiKey)
req.Header.Set(defaultAPIVersionHeader, c.apiVersion) req.Header.Set(defaultAPIVersionHeader, c.apiVersion)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return req, nil return req, nil
} }
+232
View File
@@ -0,0 +1,232 @@
package onspring
import (
"context"
"encoding/json"
"fmt"
"iter"
"net/http"
)
const (
fieldsPath = "/fields"
)
// FieldsEndpoint provides access to fields in an Onspring instance.
type FieldsEndpoint struct {
client *Client
}
// Field represents an Onspring field
type Field struct {
Id int `json:"id"`
AppId int `json:"appId"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
IsRequired bool `json:"isRequired"`
IsUnique bool `json:"isUnique"`
TypeData any `json:"-"`
}
// FormulaField represents a formula field type in Onspring
type FormulaField struct {
OutputType string `json:"outputType"`
Values []string `json:"values"`
}
// ReferenceField represents a reference field type in Onspring
type ReferenceField struct {
Multiplicity string `json:"multiplicity"`
ReferenceAppId string `json:"referenceAppId"`
}
// ListField represents a list field type in Onspring
type ListField struct {
Multiplicity string `json:"multiplicity"`
Values []string `json:"values"`
ListId int `json:"listId"`
}
// UnmarshalJSON implements the json.Unmarshaler interface for Field.
// This allows for custom deserialization logic based on the 'Type' field.
func (f *Field) UnmarshalJSON(data []byte) error {
type Alias Field
aux := &struct {
*Alias
}{
Alias: (*Alias)(f),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
switch f.Type {
case "Formula":
var formulaField FormulaField
if err := json.Unmarshal(data, &formulaField); err != nil {
return err
}
f.TypeData = formulaField
case "Reference":
var referenceField ReferenceField
if err := json.Unmarshal(data, &referenceField); err != nil {
return err
}
f.TypeData = referenceField
case "List":
var listField ListField
if err := json.Unmarshal(data, &listField); err != nil {
return err
}
f.TypeData = listField
default:
}
return nil
}
// FieldBatch represents a batch of Onspring fields
type FieldBatch struct {
Count int `json:"count"`
Items []Field `json:"items"`
}
// Get retrieves a field from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - id: The id of the field to retrieve
//
// Returns:
// - Field: A field
// - error: An error if the request fails
func (f *FieldsEndpoint) Get(ctx context.Context, id int) (Field, error) {
path := fmt.Sprintf("%s/id/%d", fieldsPath, id)
req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, nil, nil)
var field Field
if requestCreationErr != nil {
return field, requestCreationErr
}
responseErr := f.client.doWithJsonResponse(req, &field)
if responseErr != nil {
return field, responseErr
}
return field, nil
}
// GetMany retrieves a batch of fields from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - ids: The ids of the fields to retrieve
//
// Returns:
// - FieldBatch: A batch of fields
// - error: An error if the request fails
func (f *FieldsEndpoint) GetMany(ctx context.Context, ids []int) (FieldBatch, error) {
path := fmt.Sprintf("%s/batch-get", fieldsPath)
req, requestCreationErr := f.client.newRequest(ctx, http.MethodPost, path, nil, ids)
var fieldBatch FieldBatch
if requestCreationErr != nil {
return fieldBatch, requestCreationErr
}
responseErr := f.client.doWithJsonResponse(req, &fieldBatch)
if responseErr != nil {
return fieldBatch, responseErr
}
return fieldBatch, nil
}
// List retrieves a paginated list of fields from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - appId: The id of the app to retrieve fields for
// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize)
//
// Returns:
// - Page[Field]: A page of fields with pagination metadata
// - error: An error if the request fails
func (f *FieldsEndpoint) List(ctx context.Context, appId int, pagingOpts ...PagingOption) (Page[Field], error) {
pagingRequest := createPagingRequest(pagingOpts)
path := fmt.Sprintf("%s/appId/%d", fieldsPath, appId)
req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, pagingRequest.ToParams(), nil)
var page Page[Field]
if requestCreationErr != nil {
return page, requestCreationErr
}
responseErr := f.client.doWithJsonResponse(req, &page)
if responseErr != nil {
return page, responseErr
}
return page, nil
}
// ListAll returns an iterator that yields all Fields for an app across all pages.
// It automatically handles pagination by making sequential calls to List
// until all items have been retrieved or the caller stops the iteration.
//
// The iterator yields each Field and any error encountered during fetching.
// If an error occurs during a page request, the error is yielded and
// iteration terminates.
//
// Parameters:
// - ctx: The context for the request
// - appId: The id of the app to retrieve fields for
// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize)
//
// Returns:
// - iter.Seq2[Field, error]: An iterator yielding:
// - Field: The individual field record.
// - error: An error if a specific page request fails during iteration.
func (f *FieldsEndpoint) ListAll(ctx context.Context, appId int, pagingOpts ...PagingOption) iter.Seq2[Field, error] {
return func(yield func(Field, error) bool) {
pagingRequest := createPagingRequest(pagingOpts)
for {
page, err := f.List(ctx, appId, ForPageNumber(pagingRequest.PageNumber), WithPageSize(pagingRequest.PageSize))
if err != nil {
yield(Field{}, err)
return
}
for _, item := range page.Items {
if !yield(item, nil) {
return
}
}
if page.PageNumber >= page.TotalPages {
break
}
pagingRequest.PageNumber++
}
}
}
+935
View File
@@ -0,0 +1,935 @@
package onspring_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"slices"
"strconv"
"testing"
"github.com/StevanFreeborn/onspring-api-sdk-go"
)
func TestFields(t *testing.T) {
t.Run("Get", 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
_, err := client.Fields.Get(nilContext, 0)
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()
_, err := client.Fields.List(ctx, 0)
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.Fields.Get(t.Context(), 0)
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()),
)
_, err := invalidClient.Fields.Get(t.Context(), 0)
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should return an error if the /fields/id/:id endpoint returns a non-200 status code", func(t *testing.T) {
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
_, err := client.Fields.Get(t.Context(), 0)
if err == nil {
t.Errorf("Expected error, got nil")
}
})
t.Run("it should return a field if the /fields/id/:id endpoint returns a 200 status code", func(t *testing.T) {
expectedField := onspring.Field{
Id: 1,
AppId: 1,
Name: "Field",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/id/%d", expectedField.Id)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
jsonData, _ := json.Marshal(expectedField)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
field, _ := client.Fields.Get(t.Context(), expectedField.Id)
if !reflect.DeepEqual(expectedField, field) {
t.Errorf("Expected %v but got %v", expectedField, field)
}
})
})
t.Run("UnmarshalJSON", func(t *testing.T) {
t.Run("it should unmarshal a Formula field correctly", func(t *testing.T) {
jsonStr := `{
"id": 1,
"appId": 10,
"name": "Calc Field",
"type": "Formula",
"status": "Enabled",
"isRequired": true,
"isUnique": false,
"outputType": "Number",
"values": ["1", "2", "3"]
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}
if field.Type != "Formula" {
t.Errorf("Expected Type 'Formula', got %s", field.Type)
}
formulaField, ok := field.TypeData.(onspring.FormulaField)
if !ok {
t.Fatalf("Expected TypeData to be FormulaField, got %T", field.TypeData)
}
expectedFormulaField := onspring.FormulaField{
OutputType: "Number",
Values: []string{"1", "2", "3"},
}
if !reflect.DeepEqual(formulaField, expectedFormulaField) {
t.Errorf("Expected FormulaField %+v, got %+v", expectedFormulaField, formulaField)
}
})
t.Run("it should unmarshal a Reference field correctly", func(t *testing.T) {
jsonStr := `{
"id": 2,
"appId": 20,
"name": "Ref Field",
"type": "Reference",
"status": "Enabled",
"isRequired": false,
"isUnique": true,
"multiplicity": "OneToOne",
"referenceAppId": "123"
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}
if field.Type != "Reference" {
t.Errorf("Expected Type 'Reference', got %s", field.Type)
}
referenceField, ok := field.TypeData.(onspring.ReferenceField)
if !ok {
t.Fatalf("Expected TypeData to be ReferenceField, got %T", field.TypeData)
}
expectedReferenceField := onspring.ReferenceField{
Multiplicity: "OneToOne",
ReferenceAppId: "123",
}
if !reflect.DeepEqual(referenceField, expectedReferenceField) {
t.Errorf("Expected ReferenceField %+v, got %+v", expectedReferenceField, referenceField)
}
})
t.Run("it should unmarshal a List field correctly", func(t *testing.T) {
jsonStr := `{
"id": 3,
"appId": 30,
"name": "List Field",
"type": "List",
"status": "Enabled",
"isRequired": false,
"isUnique": false,
"multiplicity": "MultiSelect",
"values": ["OptionA", "OptionB"],
"listId": 456
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}
if field.Type != "List" {
t.Errorf("Expected Type 'List', got %s", field.Type)
}
listField, ok := field.TypeData.(onspring.ListField)
if !ok {
t.Fatalf("Expected TypeData to be ListField, got %T", field.TypeData)
}
expectedListField := onspring.ListField{
Multiplicity: "MultiSelect",
Values: []string{"OptionA", "OptionB"},
ListId: 456,
}
if !reflect.DeepEqual(listField, expectedListField) {
t.Errorf("Expected ListField %+v, got %+v", expectedListField, listField)
}
})
t.Run("it should handle unknown field types without TypeData", func(t *testing.T) {
jsonStr := `{
"id": 4,
"appId": 40,
"name": "Text Field",
"type": "Text",
"status": "Enabled",
"isRequired": true,
"isUnique": false
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}
if field.Type != "Text" {
t.Errorf("Expected Type 'Text', got %s", field.Type)
}
if field.TypeData != nil {
t.Errorf("Expected TypeData to be nil for 'Text' type, got %+v", field.TypeData)
}
})
t.Run("it should return an error for malformed JSON for main field struct", func(t *testing.T) {
jsonStr := `{
"id": "invalid",
"appId": 10,
"name": "Calc Field",
"type": "Formula"
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err == nil {
t.Fatalf("Expected UnmarshalJSON to fail for malformed main struct JSON, got nil")
}
})
t.Run("it should return an error for malformed JSON for TypeData (Formula)", func(t *testing.T) {
jsonStr := `{
"id": 1,
"appId": 10,
"name": "Calc Field",
"type": "Formula",
"outputType": 123,
"values": ["1", "2", "3"]
}`
var field onspring.Field
err := json.Unmarshal([]byte(jsonStr), &field)
if err == nil {
t.Fatalf("Expected UnmarshalJSON to fail for malformed Formula TypeData JSON, got nil")
}
})
})
t.Run("GetMany", 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
_, err := client.Fields.GetMany(nilContext, []int{})
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()
_, err := client.Fields.GetMany(ctx, []int{})
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.Fields.GetMany(t.Context(), []int{})
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()),
)
_, err := invalidClient.Fields.GetMany(t.Context(), []int{})
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should return an error if the /fields/batch-get endpoint returns a non-200 status code", func(t *testing.T) {
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
_, err := client.Fields.GetMany(t.Context(), []int{})
if err == nil {
t.Errorf("Expected error, got nil")
}
})
t.Run("it should return a batch of fields when the /fields/batch-get endpoint returns a 200 status code", func(t *testing.T) {
fields := []onspring.Field{
{
Id: 1,
AppId: 1,
Name: "Field 1",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
}
expectedBatch := onspring.FieldBatch{
Count: len(fields),
Items: fields,
}
expectedIds := []int{fields[0].Id}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("Expected POST method, got %s", r.Method)
}
if r.URL.Path != "/fields/batch-get" {
t.Errorf("Expected /fields/batch-get endpoint, got %s", r.URL.Path)
}
var ids []int
err := json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
t.Errorf("Expected to decode request body, but got error: %v", err)
}
if !slices.Equal(expectedIds, ids) {
t.Errorf("Expected body to be %v but got %v", expectedIds, ids)
}
jsonData, _ := json.Marshal(expectedBatch)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
batch, _ := client.Fields.GetMany(t.Context(), expectedIds)
if !reflect.DeepEqual(expectedBatch, batch) {
t.Errorf("Expected %v but got %v", expectedBatch, batch)
}
})
})
t.Run("List", 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
_, err := client.Fields.List(nilContext, 0)
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()
_, err := client.Fields.List(ctx, 0)
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.Fields.List(t.Context(), 0)
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()),
)
_, err := invalidClient.Fields.List(t.Context(), 0)
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should return a page of fields if the /fields/appId/:appId endpoint returns a 200 status code", func(t *testing.T) {
appId := 1
expectedPageNumber := 1
expectedPageSize := 50
expectedPage := onspring.Page[onspring.Field]{
TotalPages: 1,
TotalRecords: 1,
PageNumber: 1,
PageSize: 50,
Items: []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
},
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
pageSize := r.URL.Query().Get("pageSize")
if pageNumber != strconv.Itoa(expectedPageNumber) {
t.Errorf("Expected query param pageNumber to be %d but got %s", expectedPageNumber, pageNumber)
}
if pageSize != strconv.Itoa(expectedPageSize) {
t.Errorf("Expected query param pageSize to be %d but got %s", expectedPageSize, pageSize)
}
jsonData, _ := json.Marshal(expectedPage)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
page, err := client.Fields.List(t.Context(), appId)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(expectedPage, page) {
t.Errorf("Expected %v but got %v", expectedPage, page)
}
})
t.Run("it should perform a GET request to the /fields/appId/:appId endpoint with non-default paging information when provided", func(t *testing.T) {
appId := 1
expectedPageNumber := 2
expectedPageSize := 1
expectedPage := onspring.Page[onspring.Field]{
TotalPages: 1,
TotalRecords: 1,
PageNumber: 1,
PageSize: 1,
Items: []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
},
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
pageSize := r.URL.Query().Get("pageSize")
if pageNumber != strconv.Itoa(expectedPageNumber) {
t.Errorf("Expected query param pageNumber to be %d but got %s", expectedPageNumber, pageNumber)
}
if pageSize != strconv.Itoa(expectedPageSize) {
t.Errorf("Expected query param pageSize to be %d but got %s", expectedPageSize, pageSize)
}
jsonData, _ := json.Marshal(expectedPage)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
_, _ = client.Fields.List(
t.Context(),
appId,
onspring.ForPageNumber(expectedPageNumber),
onspring.WithPageSize(expectedPageSize),
)
})
t.Run("it should return an error if the /fields/appId/:appId endpoint returns a non-200 status code", func(t *testing.T) {
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
_, err := client.Fields.List(t.Context(), 0)
if err == nil {
t.Errorf("Expected error, got nil")
}
})
})
t.Run("ListAll", func(t *testing.T) {
t.Run("it should return an error if fails to retrieve any pages of fields", func(t *testing.T) {
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
for _, err := range client.Fields.ListAll(t.Context(), 0) {
if err == nil {
t.Errorf("Expected error, got nil")
}
}
})
t.Run("it should return all the fields from multiple pages", func(t *testing.T) {
appId := 1
expectedFields := []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field 1",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
{
Id: 2,
AppId: appId,
Name: "Field 2",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
}
pageOne := onspring.Page[onspring.Field]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 1,
PageSize: 1,
Items: []onspring.Field{expectedFields[0]},
}
pageTwo := onspring.Page[onspring.Field]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 2,
PageSize: 1,
Items: []onspring.Field{expectedFields[1]},
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "1" {
jsonData, _ := json.Marshal(pageOne)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
if pageNumber == "2" {
jsonData, _ := json.Marshal(pageTwo)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedFields := []onspring.Field{}
for field, _ := range client.Fields.ListAll(t.Context(), appId) {
retrievedFields = append(retrievedFields, field)
}
if !slices.Equal(expectedFields, retrievedFields) {
t.Errorf("Expected %v but got %v", expectedFields, retrievedFields)
}
})
t.Run("it should return fields and errors if some pages fail and some succeed", func(t *testing.T) {
appId := 1
expectedFields := []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field 1",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
{
Id: 2,
AppId: appId,
Name: "Field 2",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
}
page := onspring.Page[onspring.Field]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 1,
PageSize: 1,
Items: expectedFields,
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "1" {
jsonData, _ := json.Marshal(page)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
if pageNumber == "2" {
w.WriteHeader(http.StatusInternalServerError)
}
})
retrievedFields := []onspring.Field{}
encounteredErrors := []error{}
for field, err := range client.Fields.ListAll(t.Context(), appId) {
if err != nil {
encounteredErrors = append(encounteredErrors, err)
} else {
retrievedFields = append(retrievedFields, field)
}
}
if !slices.Equal(expectedFields, retrievedFields) {
t.Errorf("Expected %v but got %v", expectedFields, retrievedFields)
}
if len(encounteredErrors) != 1 {
t.Errorf("Expected to receive one error, but received %d", len(encounteredErrors))
}
})
t.Run("it should start paging from specified page number when given", func(t *testing.T) {
appId := 1
expectedFields := []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field 1",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
{
Id: 2,
AppId: appId,
Name: "Field 2",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
}
pageTwo := onspring.Page[onspring.Field]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 2,
PageSize: 1,
Items: expectedFields,
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "2" {
jsonData, _ := json.Marshal(pageTwo)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedFields := []onspring.Field{}
for field, _ := range client.Fields.ListAll(t.Context(), appId, onspring.ForPageNumber(2)) {
retrievedFields = append(retrievedFields, field)
}
if !slices.Equal(expectedFields, retrievedFields) {
t.Errorf("Expected %v but got %v", expectedFields, retrievedFields)
}
})
t.Run("it should retrieve pages using specified page size when given", func(t *testing.T) {
appId := 1
expectedFields := []onspring.Field{
{
Id: 1,
AppId: appId,
Name: "Field 1",
Type: "Text",
Status: "Enabled",
IsRequired: true,
IsUnique: false,
},
}
page := onspring.Page[onspring.Field]{
TotalPages: 1,
TotalRecords: 1,
PageNumber: 1,
PageSize: 1,
Items: expectedFields,
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/fields/appId/%d", appId)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
pageSize := r.URL.Query().Get("pageSize")
if pageSize == "1" {
jsonData, _ := json.Marshal(page)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedFields := []onspring.Field{}
for field, _ := range client.Fields.ListAll(t.Context(), appId, onspring.WithPageSize(1)) {
retrievedFields = append(retrievedFields, field)
}
if !slices.Equal(expectedFields, retrievedFields) {
t.Errorf("Expected %v but got %v", expectedFields, retrievedFields)
}
})
})
}
+13
View File
@@ -34,3 +34,16 @@ func WithPageSize(pageSize int) PagingOption {
pr.PageSize = pageSize pr.PageSize = pageSize
} }
} }
func createPagingRequest(pagingOpts []PagingOption) *PagingRequest {
pagingRequest := &PagingRequest{
PageNumber: 1,
PageSize: 50,
}
for _, opt := range pagingOpts {
opt(pagingRequest)
}
return pagingRequest
}