diff --git a/README.md b/README.md index 2dd148a..16fa5e7 100644 --- a/README.md +++ b/README.md @@ -479,4 +479,227 @@ if err != nil { } else { fmt.Printf("Saved File Id: %d\n", response.Id) } +``` + +### Records + +#### Get Record by Id + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +record, err := client.Records.Get(context.TODO(), 1, 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Record: %+v\n", record) +} +``` + +You can also specify which fields to include and the data format: + +```go +record, err := client.Records.Get( + context.TODO(), 1, 1, + onspring.WithFieldIds([]int{1, 2, 3}), + onspring.WithRecordDataFormat("Formatted"), +) +``` + +#### Get Records 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.Records.List(context.TODO(), 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Page %d of Records: %+v\n", page.PageNumber, page.Items) +} +``` + +You can combine paging, field, and data format options: + +```go +page, err := client.Records.List( + context.TODO(), 1, + onspring.WithFieldIds([]int{1, 2}), + onspring.WithRecordDataFormat("Formatted"), + onspring.WithPaging(onspring.ForPageNumber(2), onspring.WithPageSize(10)), +) +``` + +##### Retrieve all pages + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +for record, err := range client.Records.ListAll(context.TODO(), 1) { + if err != nil { + fmt.Printf("Error during iteration: %v\n", err) + break + } + fmt.Printf("Retrieved Record: %+v\n", record) +} +``` + +#### Get Records by Batch + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +batch, err := client.Records.GetMany(context.TODO(), onspring.GetManyRecordsRequest{ + AppId: 1, + RecordIds: []int{1, 2}, + FieldIds: []int{1, 2}, +}) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Record Batch (Count: %d): %+v\n", batch.Count, batch.Items) +} +``` + +#### Query Records + +##### 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.Records.Query(context.TODO(), onspring.QueryRecordsRequest{ + AppId: 1, + Filter: "field eq 'value'", +}) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Page %d of Records: %+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 record, err := range client.Records.QueryAll(context.TODO(), onspring.QueryRecordsRequest{ + AppId: 1, + Filter: "field eq 'value'", +}) { + if err != nil { + fmt.Printf("Error during iteration: %v\n", err) + break + } + fmt.Printf("Retrieved Record: %+v\n", record) +} +``` + +#### Save Record + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +response, err := client.Records.Save(context.TODO(), onspring.SaveRecordRequest{ + AppId: 1, + Fields: map[string]any{"1": "value", "2": 42}, +}) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Saved Record Id: %d\n", response.Id) +} +``` + +#### Delete Record + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +err := client.Records.Delete(context.TODO(), 1, 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Println("Record deleted successfully!") +} +``` + +#### Delete Records by Batch + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +err := client.Records.DeleteMany(context.TODO(), onspring.DeleteManyRecordsRequest{ + AppId: 1, + RecordIds: []int{1, 2, 3}, +}) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Println("Records deleted successfully!") +} diff --git a/client.go b/client.go index 9439469..317961d 100644 --- a/client.go +++ b/client.go @@ -53,6 +53,8 @@ type Client struct { Reports *ReportsEndpoint // Files provides access to the files within an Onspring instance. Files *FilesEndpoint + // Records provides access to the records within an Onspring instance. + Records *RecordsEndpoint } // NewClient creates a new Onspring API client with the provided API key. @@ -91,6 +93,7 @@ func NewClient(apiKey string, opts ...ClientOption) *Client { c.Lists = &ListsEndpoint{client: c} c.Reports = &ReportsEndpoint{client: c} c.Files = &FilesEndpoint{client: c} + c.Records = &RecordsEndpoint{client: c} return c } diff --git a/records.go b/records.go new file mode 100644 index 0000000..a1b014d --- /dev/null +++ b/records.go @@ -0,0 +1,432 @@ +package onspring + +import ( + "context" + "fmt" + "iter" + "net/http" + "strconv" + "strings" +) + +const ( + recordsPath = "/records" +) + +// RecordsEndpoint provides access to records in an Onspring instance. +type RecordsEndpoint struct { + client *Client +} + +// Record represents an Onspring record. +type Record struct { + AppId int `json:"appId"` + RecordId int `json:"recordId"` + FieldData []RecordFieldValue `json:"fieldData"` +} + +// RecordFieldValue represents a field value within a record. +type RecordFieldValue struct { + Type string `json:"type"` + FieldId int `json:"fieldId"` + Value any `json:"value"` +} + +// RecordBatch represents a batch of Onspring records. +type RecordBatch struct { + Count int `json:"count"` + Items []Record `json:"items"` +} + +// RecordOption is a functional option for configuring record requests. +type RecordOption func(*recordRequest) + +type recordRequest struct { + FieldIds []int + DataFormat string + PagingRequest PagingRequest +} + +func (r *recordRequest) ToParams() map[string]string { + params := r.PagingRequest.ToParams() + + if len(r.FieldIds) > 0 { + ids := make([]string, len(r.FieldIds)) + + for i, id := range r.FieldIds { + ids[i] = strconv.Itoa(id) + } + + params["fieldIds"] = strings.Join(ids, ",") + } + + if r.DataFormat != "" { + params["dataFormat"] = r.DataFormat + } + + return params +} + +func (r *recordRequest) ToQueryParams() map[string]string { + params := map[string]string{} + + if len(r.FieldIds) > 0 { + ids := make([]string, len(r.FieldIds)) + + for i, id := range r.FieldIds { + ids[i] = strconv.Itoa(id) + } + + params["fieldIds"] = strings.Join(ids, ",") + } + + if r.DataFormat != "" { + params["dataFormat"] = r.DataFormat + } + + return params +} + +func createRecordRequest(opts []RecordOption) *recordRequest { + r := &recordRequest{ + PagingRequest: PagingRequest{PageNumber: 1, PageSize: 50}, + } + + for _, opt := range opts { + opt(r) + } + + return r +} + +// WithFieldIds sets the field identifiers to include in the record response. +func WithFieldIds(ids []int) RecordOption { + return func(r *recordRequest) { + r.FieldIds = ids + } +} + +// WithRecordDataFormat sets the data format for the record response. +// Valid values are "Raw" and "Formatted". +func WithRecordDataFormat(format string) RecordOption { + return func(r *recordRequest) { + r.DataFormat = format + } +} + +// WithPaging applies paging options to a record request. +func WithPaging(opts ...PagingOption) RecordOption { + return func(r *recordRequest) { + for _, opt := range opts { + opt(&r.PagingRequest) + } + } +} + +// GetManyRecordsRequest represents a request to get a batch of records. +type GetManyRecordsRequest struct { + AppId int `json:"appId"` + RecordIds []int `json:"recordIds"` + FieldIds []int `json:"fieldIds,omitempty"` + DataFormat string `json:"dataFormat,omitempty"` +} + +// QueryRecordsRequest represents a request to query records. +type QueryRecordsRequest struct { + AppId int `json:"appId"` + Filter string `json:"filter"` + FieldIds []int `json:"fieldIds,omitempty"` + DataFormat string `json:"dataFormat,omitempty"` +} + +// SaveRecordRequest represents a request to save a record. +type SaveRecordRequest struct { + AppId int `json:"appId"` + RecordId *int `json:"recordId,omitempty"` + Fields map[string]any `json:"fields"` +} + +// SaveRecordResponse represents the response for saving a record. +type SaveRecordResponse struct { + Id int `json:"id"` + Warnings []string `json:"warnings"` +} + +// DeleteManyRecordsRequest represents a request to delete a batch of records. +type DeleteManyRecordsRequest struct { + AppId int `json:"appId"` + RecordIds []int `json:"recordIds"` +} + +// Get retrieves a record from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - appId: The id of the app +// - recordId: The id of the record to retrieve +// - opts: Optional record configuration functions (e.g., WithFieldIds, WithRecordDataFormat) +// +// Returns: +// - Record: A record +// - error: An error if the request fails +func (rc *RecordsEndpoint) Get(ctx context.Context, appId, recordId int, opts ...RecordOption) (Record, error) { + recordReq := createRecordRequest(opts) + path := fmt.Sprintf("%s/appId/%d/recordId/%d", recordsPath, appId, recordId) + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodGet, path, recordReq.ToQueryParams(), nil) + + var record Record + + if requestCreationErr != nil { + return record, requestCreationErr + } + + responseErr := rc.client.doWithJsonResponse(req, &record) + + if responseErr != nil { + return record, responseErr + } + + return record, nil +} + +// List retrieves a paginated list of records for an app from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - appId: The id of the app to retrieve records for +// - opts: Optional record configuration functions (e.g., WithFieldIds, WithRecordDataFormat, WithPaging) +// +// Returns: +// - Page[Record]: A page of records with pagination metadata +// - error: An error if the request fails +func (rc *RecordsEndpoint) List(ctx context.Context, appId int, opts ...RecordOption) (Page[Record], error) { + recordReq := createRecordRequest(opts) + path := fmt.Sprintf("%s/appId/%d", recordsPath, appId) + + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodGet, path, recordReq.ToParams(), nil) + + var page Page[Record] + + if requestCreationErr != nil { + return page, requestCreationErr + } + + responseErr := rc.client.doWithJsonResponse(req, &page) + + if responseErr != nil { + return page, responseErr + } + + return page, nil +} + +// ListAll returns an iterator that yields all Records 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. +// +// Parameters: +// - ctx: The context for the request +// - appId: The id of the app to retrieve records for +// - opts: Optional record configuration functions (e.g., WithFieldIds, WithRecordDataFormat, WithPaging) +// +// Returns: +// - iter.Seq2[Record, error]: An iterator yielding: +// - Record: The individual record. +// - error: An error if a specific page request fails during iteration. +func (rc *RecordsEndpoint) ListAll(ctx context.Context, appId int, opts ...RecordOption) iter.Seq2[Record, error] { + return func(yield func(Record, error) bool) { + recordReq := createRecordRequest(opts) + + for { + page, err := rc.List( + ctx, + appId, + WithFieldIds(recordReq.FieldIds), + WithRecordDataFormat(recordReq.DataFormat), + WithPaging(ForPageNumber(recordReq.PagingRequest.PageNumber), WithPageSize(recordReq.PagingRequest.PageSize)), + ) + + if err != nil { + yield(Record{}, err) + return + } + + for _, item := range page.Items { + if !yield(item, nil) { + return + } + } + + if page.PageNumber >= page.TotalPages { + break + } + + recordReq.PagingRequest.PageNumber++ + } + } +} + +// GetMany retrieves a batch of records from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - request: The batch get request containing app id, record ids, and optional field ids/data format +// +// Returns: +// - RecordBatch: A batch of records +// - error: An error if the request fails +func (rc *RecordsEndpoint) GetMany(ctx context.Context, request GetManyRecordsRequest) (RecordBatch, error) { + path := fmt.Sprintf("%s/batch-get", recordsPath) + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodPost, path, nil, request) + + var batch RecordBatch + + if requestCreationErr != nil { + return batch, requestCreationErr + } + + responseErr := rc.client.doWithJsonResponse(req, &batch) + + if responseErr != nil { + return batch, responseErr + } + + return batch, nil +} + +// Query queries records from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - request: The query request containing app id, filter, and optional field ids/data format +// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize) +// +// Returns: +// - Page[Record]: A page of records with pagination metadata +// - error: An error if the request fails +func (rc *RecordsEndpoint) Query(ctx context.Context, request QueryRecordsRequest, pagingOpts ...PagingOption) (Page[Record], error) { + pagingRequest := createPagingRequest(pagingOpts) + path := fmt.Sprintf("%s/query", recordsPath) + + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodPost, path, pagingRequest.ToParams(), request) + + var page Page[Record] + + if requestCreationErr != nil { + return page, requestCreationErr + } + + responseErr := rc.client.doWithJsonResponse(req, &page) + + if responseErr != nil { + return page, responseErr + } + + return page, nil +} + +// QueryAll returns an iterator that yields all Records matching a query across all pages. +// It automatically handles pagination by making sequential calls to Query +// until all items have been retrieved or the caller stops the iteration. +// +// Parameters: +// - ctx: The context for the request +// - request: The query request containing app id, filter, and optional field ids/data format +// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize) +// +// Returns: +// - iter.Seq2[Record, error]: An iterator yielding: +// - Record: The individual record. +// - error: An error if a specific page request fails during iteration. +func (rc *RecordsEndpoint) QueryAll(ctx context.Context, request QueryRecordsRequest, pagingOpts ...PagingOption) iter.Seq2[Record, error] { + return func(yield func(Record, error) bool) { + pagingRequest := createPagingRequest(pagingOpts) + + for { + page, err := rc.Query(ctx, request, ForPageNumber(pagingRequest.PageNumber), WithPageSize(pagingRequest.PageSize)) + + if err != nil { + yield(Record{}, err) + return + } + + for _, item := range page.Items { + if !yield(item, nil) { + return + } + } + + if page.PageNumber >= page.TotalPages { + break + } + + pagingRequest.PageNumber++ + } + } +} + +// Save creates or updates a record in the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - request: The save request containing app id, optional record id, and field values +// +// Returns: +// - SaveRecordResponse: The response containing the saved record's id and any warnings +// - error: An error if the request fails +func (rc *RecordsEndpoint) Save(ctx context.Context, request SaveRecordRequest) (SaveRecordResponse, error) { + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodPut, recordsPath, nil, request) + + var response SaveRecordResponse + + if requestCreationErr != nil { + return response, requestCreationErr + } + + responseErr := rc.client.doWithJsonResponse(req, &response) + + if responseErr != nil { + return response, responseErr + } + + return response, nil +} + +// Delete removes a record from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - appId: The id of the app +// - recordId: The id of the record to delete +// +// Returns: +// - error: An error if the request fails +func (rc *RecordsEndpoint) Delete(ctx context.Context, appId, recordId int) error { + path := fmt.Sprintf("%s/appId/%d/recordId/%d", recordsPath, appId, recordId) + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodDelete, path, nil, nil) + + if requestCreationErr != nil { + return requestCreationErr + } + + return rc.client.do(req) +} + +// DeleteMany removes a batch of records from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - request: The batch delete request containing app id and record ids +// +// Returns: +// - error: An error if the request fails +func (rc *RecordsEndpoint) DeleteMany(ctx context.Context, request DeleteManyRecordsRequest) error { + path := fmt.Sprintf("%s/batch-delete", recordsPath) + req, requestCreationErr := rc.client.newRequest(ctx, http.MethodPost, path, nil, request) + + if requestCreationErr != nil { + return requestCreationErr + } + + return rc.client.do(req) +} diff --git a/records_test.go b/records_test.go new file mode 100644 index 0000000..7775485 --- /dev/null +++ b/records_test.go @@ -0,0 +1,1174 @@ +package onspring_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "strconv" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestRecords(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.Records.Get(nilContext, 1, 1) + + 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.Records.Get(ctx, 1, 1) + + 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.Records.Get(t.Context(), 1, 1) + + 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.Records.Get(t.Context(), 1, 1) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the 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.Records.Get(t.Context(), 1, 1) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a GET request to the correct endpoint and return a record", func(t *testing.T) { + appId := 1 + recordId := 2 + + expectedRecord := onspring.Record{ + AppId: appId, + RecordId: recordId, + FieldData: []onspring.RecordFieldValue{ + { + Type: "String", + FieldId: 1, + Value: "test value", + }, + }, + } + + _, 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("/records/appId/%d/recordId/%d", appId, recordId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + jsonData, _ := json.Marshal(expectedRecord) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + record, err := client.Records.Get(t.Context(), appId, recordId) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if record.AppId != expectedRecord.AppId { + t.Errorf("Expected AppId %d but got %d", expectedRecord.AppId, record.AppId) + } + + if record.RecordId != expectedRecord.RecordId { + t.Errorf("Expected RecordId %d but got %d", expectedRecord.RecordId, record.RecordId) + } + + if len(record.FieldData) != len(expectedRecord.FieldData) { + t.Errorf("Expected %d field data items but got %d", len(expectedRecord.FieldData), len(record.FieldData)) + } + }) + + t.Run("it should include fieldIds query param when provided", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + fieldIds := r.URL.Query().Get("fieldIds") + + if fieldIds != "1,2,3" { + t.Errorf("Expected query param fieldIds to be 1,2,3 but got %s", fieldIds) + } + + jsonData, _ := json.Marshal(onspring.Record{}) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + _, _ = client.Records.Get(t.Context(), 1, 1, onspring.WithFieldIds([]int{1, 2, 3})) + }) + + t.Run("it should include dataFormat query param when provided", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + dataFormat := r.URL.Query().Get("dataFormat") + + if dataFormat != "Formatted" { + t.Errorf("Expected query param dataFormat to be Formatted but got %s", dataFormat) + } + + jsonData, _ := json.Marshal(onspring.Record{}) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + _, _ = client.Records.Get(t.Context(), 1, 1, onspring.WithRecordDataFormat("Formatted")) + }) + }) + + 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.Records.List(nilContext, 1) + + 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.Records.List(ctx, 1) + + 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.Records.List(t.Context(), 1) + + 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.Records.List(t.Context(), 1) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the 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.Records.List(t.Context(), 1) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a GET request to the correct endpoint and return page of records", func(t *testing.T) { + appId := 1 + expectedPageNumber := 1 + expectedPageSize := 50 + + expectedPage := onspring.Page[onspring.Record]{ + TotalPages: 1, + TotalRecords: 1, + PageNumber: 1, + PageSize: 1, + Items: []onspring.Record{ + { + AppId: appId, + RecordId: 1, + FieldData: []onspring.RecordFieldValue{}, + }, + }, + } + + _, 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("/records/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.Records.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 include paging and record options when provided", func(t *testing.T) { + appId := 1 + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + pageNumber := r.URL.Query().Get("pageNumber") + + if pageNumber != "2" { + t.Errorf("Expected query param pageNumber to be 2 but got %s", pageNumber) + } + + pageSize := r.URL.Query().Get("pageSize") + + if pageSize != "10" { + t.Errorf("Expected query param pageSize to be 10 but got %s", pageSize) + } + + fieldIds := r.URL.Query().Get("fieldIds") + + if fieldIds != "1,2" { + t.Errorf("Expected query param fieldIds to be 1,2 but got %s", fieldIds) + } + + dataFormat := r.URL.Query().Get("dataFormat") + + if dataFormat != "Formatted" { + t.Errorf("Expected query param dataFormat to be Formatted but got %s", dataFormat) + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"pageNumber":2,"pageSize":10,"totalPages":2,"totalRecords":2,"items":[]}`)) + }) + + _, _ = client.Records.List( + t.Context(), + appId, + onspring.WithFieldIds([]int{1, 2}), + onspring.WithRecordDataFormat("Formatted"), + onspring.WithPaging(onspring.ForPageNumber(2), onspring.WithPageSize(10)), + ) + }) + }) + + t.Run("ListAll", func(t *testing.T) { + t.Run("it should return an error if fails to retrieve any pages of records", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + for _, err := range client.Records.ListAll(t.Context(), 1) { + if err == nil { + t.Errorf("Expected error, got nil") + } + } + }) + + t.Run("it should return all the records from multiple pages", func(t *testing.T) { + expectedRecords := []onspring.Record{ + {AppId: 1, RecordId: 1, FieldData: []onspring.RecordFieldValue{}}, + {AppId: 1, RecordId: 2, FieldData: []onspring.RecordFieldValue{}}, + } + + pageOne := onspring.Page[onspring.Record]{ + TotalPages: 2, TotalRecords: 2, PageNumber: 1, PageSize: 1, + Items: []onspring.Record{expectedRecords[0]}, + } + + pageTwo := onspring.Page[onspring.Record]{ + TotalPages: 2, TotalRecords: 2, PageNumber: 2, PageSize: 1, + Items: []onspring.Record{expectedRecords[1]}, + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + 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) + } + }) + + retrievedRecords := []onspring.Record{} + + for record, _ := range client.Records.ListAll(t.Context(), 1) { + retrievedRecords = append(retrievedRecords, record) + } + + if !reflect.DeepEqual(expectedRecords, retrievedRecords) { + t.Errorf("Expected %v but got %v", expectedRecords, retrievedRecords) + } + }) + + t.Run("it should return records and errors if some pages fail and some succeed", func(t *testing.T) { + expectedRecords := []onspring.Record{ + {AppId: 1, RecordId: 1, FieldData: []onspring.RecordFieldValue{}}, + } + + pageOne := onspring.Page[onspring.Record]{ + TotalPages: 2, TotalRecords: 2, PageNumber: 1, PageSize: 1, + Items: []onspring.Record{expectedRecords[0]}, + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + 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" { + w.WriteHeader(http.StatusInternalServerError) + } + }) + + retrievedRecords := []onspring.Record{} + encounteredErrors := []error{} + + for record, err := range client.Records.ListAll(t.Context(), 1) { + if err != nil { + encounteredErrors = append(encounteredErrors, err) + } else { + retrievedRecords = append(retrievedRecords, record) + } + } + + if !reflect.DeepEqual(expectedRecords, retrievedRecords) { + t.Errorf("Expected %v but got %v", expectedRecords, retrievedRecords) + } + + if len(encounteredErrors) != 1 { + t.Errorf("Expected to receive one error, but received %d", len(encounteredErrors)) + } + }) + + t.Run("it should pass record options through to each page request", func(t *testing.T) { + page := onspring.Page[onspring.Record]{ + TotalPages: 1, TotalRecords: 0, PageNumber: 1, PageSize: 1, + Items: []onspring.Record{}, + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + fieldIds := r.URL.Query().Get("fieldIds") + + if fieldIds != "1,2" { + t.Errorf("Expected query param fieldIds to be 1,2 but got %s", fieldIds) + } + + jsonData, _ := json.Marshal(page) + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + for range client.Records.ListAll(t.Context(), 1, onspring.WithFieldIds([]int{1, 2})) { + } + }) + }) + + 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.Records.GetMany(nilContext, onspring.GetManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.GetMany(ctx, onspring.GetManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.GetMany(t.Context(), onspring.GetManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.GetMany(t.Context(), onspring.GetManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the 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.Records.GetMany(t.Context(), onspring.GetManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a POST request to the /records/batch-get endpoint and return a batch of records", func(t *testing.T) { + request := onspring.GetManyRecordsRequest{ + AppId: 1, + RecordIds: []int{1, 2}, + FieldIds: []int{1}, + DataFormat: "Formatted", + } + + expectedBatch := onspring.RecordBatch{ + Count: 2, + Items: []onspring.Record{ + {AppId: 1, RecordId: 1, FieldData: []onspring.RecordFieldValue{}}, + {AppId: 1, RecordId: 2, FieldData: []onspring.RecordFieldValue{}}, + }, + } + + _, 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 != "/records/batch-get" { + t.Errorf("Expected /records/batch-get endpoint, got %s", r.URL.Path) + } + + var body onspring.GetManyRecordsRequest + 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(request, body) { + t.Errorf("Expected body to be %v but got %v", request, body) + } + + jsonData, _ := json.Marshal(expectedBatch) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + batch, err := client.Records.GetMany(t.Context(), request) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !reflect.DeepEqual(expectedBatch, batch) { + t.Errorf("Expected %v but got %v", expectedBatch, batch) + } + }) + }) + + t.Run("Query", 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.Records.Query(nilContext, onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) + + 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.Records.Query(ctx, onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) + + 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.Records.Query(t.Context(), onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) + + 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.Records.Query(t.Context(), onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the 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.Records.Query(t.Context(), onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a POST request to the /records/query endpoint and return page of records", func(t *testing.T) { + request := onspring.QueryRecordsRequest{ + AppId: 1, + Filter: "field eq 'value'", + FieldIds: []int{1}, + DataFormat: "Formatted", + } + + expectedPage := onspring.Page[onspring.Record]{ + TotalPages: 1, + TotalRecords: 1, + PageNumber: 1, + PageSize: 1, + Items: []onspring.Record{ + {AppId: 1, RecordId: 1, FieldData: []onspring.RecordFieldValue{}}, + }, + } + + _, 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 != "/records/query" { + t.Errorf("Expected /records/query endpoint, got %s", r.URL.Path) + } + + var body onspring.QueryRecordsRequest + 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(request, body) { + t.Errorf("Expected body to be %v but got %v", request, body) + } + + jsonData, _ := json.Marshal(expectedPage) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + page, err := client.Records.Query(t.Context(), request) + + 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 include paging query params when provided", func(t *testing.T) { + expectedPageNumber := 2 + expectedPageSize := 10 + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + 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) + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"pageNumber":2,"pageSize":10,"totalPages":2,"totalRecords":2,"items":[]}`)) + }) + + _, _ = client.Records.Query( + t.Context(), + onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}, + onspring.ForPageNumber(expectedPageNumber), + onspring.WithPageSize(expectedPageSize), + ) + }) + }) + + t.Run("QueryAll", func(t *testing.T) { + t.Run("it should return an error if fails to retrieve any pages", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + for _, err := range client.Records.QueryAll(t.Context(), onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) { + if err == nil { + t.Errorf("Expected error, got nil") + } + } + }) + + t.Run("it should return all the records from multiple pages", func(t *testing.T) { + expectedRecords := []onspring.Record{ + {AppId: 1, RecordId: 1, FieldData: []onspring.RecordFieldValue{}}, + {AppId: 1, RecordId: 2, FieldData: []onspring.RecordFieldValue{}}, + } + + pageOne := onspring.Page[onspring.Record]{ + TotalPages: 2, TotalRecords: 2, PageNumber: 1, PageSize: 1, + Items: []onspring.Record{expectedRecords[0]}, + } + + pageTwo := onspring.Page[onspring.Record]{ + TotalPages: 2, TotalRecords: 2, PageNumber: 2, PageSize: 1, + Items: []onspring.Record{expectedRecords[1]}, + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + 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) + } + }) + + retrievedRecords := []onspring.Record{} + + for record, _ := range client.Records.QueryAll(t.Context(), onspring.QueryRecordsRequest{AppId: 1, Filter: "field eq 'value'"}) { + retrievedRecords = append(retrievedRecords, record) + } + + if !reflect.DeepEqual(expectedRecords, retrievedRecords) { + t.Errorf("Expected %v but got %v", expectedRecords, retrievedRecords) + } + }) + }) + + 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 + + _, err := client.Records.Save(nilContext, onspring.SaveRecordRequest{AppId: 1, Fields: map[string]any{}}) + + 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.Records.Save(ctx, onspring.SaveRecordRequest{AppId: 1, Fields: map[string]any{}}) + + 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.Records.Save(t.Context(), onspring.SaveRecordRequest{AppId: 1, Fields: map[string]any{}}) + + 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.Records.Save(t.Context(), onspring.SaveRecordRequest{AppId: 1, Fields: map[string]any{}}) + + 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.Records.Save(t.Context(), onspring.SaveRecordRequest{AppId: 1, Fields: map[string]any{}}) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a PUT request to the /records endpoint and return a response when successful", func(t *testing.T) { + recordId := 1 + request := onspring.SaveRecordRequest{ + AppId: 1, + RecordId: &recordId, + Fields: map[string]any{"1": "value"}, + } + + expectedResponse := onspring.SaveRecordResponse{ + Id: 1, + Warnings: []string{"warning"}, + } + + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("Expected PUT method, got %s", r.Method) + } + + if r.URL.Path != "/records" { + t.Errorf("Expected /records endpoint, got %s", r.URL.Path) + } + + var body onspring.SaveRecordRequest + 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(request, body) { + t.Errorf("Expected body to be %v but got %v", request, body) + } + + jsonData, _ := json.Marshal(expectedResponse) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + response, err := client.Records.Save(t.Context(), request) + + 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.Records.Delete(nilContext, 1, 1) + + 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.Records.Delete(ctx, 1, 1) + + 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.Records.Delete(t.Context(), 1, 1) + + 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.Records.Delete(t.Context(), 1, 1) + + 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.Records.Delete(t.Context(), 1, 1) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a DELETE request to the correct endpoint and return no error if receives 204 status code", func(t *testing.T) { + appId := 1 + recordId := 2 + + _, 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("/records/appId/%d/recordId/%d", appId, recordId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + w.WriteHeader(http.StatusNoContent) + }) + + err := client.Records.Delete(t.Context(), appId, recordId) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + }) + + t.Run("DeleteMany", 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.Records.DeleteMany(nilContext, onspring.DeleteManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.DeleteMany(ctx, onspring.DeleteManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.DeleteMany(t.Context(), onspring.DeleteManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.DeleteMany(t.Context(), onspring.DeleteManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + 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.Records.DeleteMany(t.Context(), onspring.DeleteManyRecordsRequest{AppId: 1, RecordIds: []int{1}}) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should perform a POST request to the /records/batch-delete endpoint and return no error if receives 204 status code", func(t *testing.T) { + request := onspring.DeleteManyRecordsRequest{ + AppId: 1, + RecordIds: []int{1, 2}, + } + + _, 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 != "/records/batch-delete" { + t.Errorf("Expected /records/batch-delete endpoint, got %s", r.URL.Path) + } + + var body onspring.DeleteManyRecordsRequest + 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(request, body) { + t.Errorf("Expected body to be %v but got %v", request, body) + } + + w.WriteHeader(http.StatusNoContent) + }) + + err := client.Records.DeleteMany(t.Context(), request) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + }) +}