feat: implement files endpoint
This commit is contained in:
@@ -380,4 +380,103 @@ for report, err := range client.Reports.ListAll(context.TODO(), 1) {
|
|||||||
}
|
}
|
||||||
fmt.Printf("Retrieved Report: %+v\n", report)
|
fmt.Printf("Retrieved Report: %+v\n", report)
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
#### Get File Info
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := onspring.NewClient("your-api-key")
|
||||||
|
|
||||||
|
fileInfo, err := client.Files.GetInfo(context.TODO(), 1, 2, 3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("File Name: %s\n", fileInfo.Name)
|
||||||
|
fmt.Printf("Content Type: %s\n", fileInfo.ContentType)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get File Content
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := onspring.NewClient("your-api-key")
|
||||||
|
|
||||||
|
fileContent, err := client.Files.GetContent(context.TODO(), 1, 2, 3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("File Name: %s\n", fileContent.FileName)
|
||||||
|
fmt.Printf("Content Type: %s\n", fileContent.ContentType)
|
||||||
|
os.WriteFile(fileContent.FileName, fileContent.Data, 0644)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete File
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := onspring.NewClient("your-api-key")
|
||||||
|
|
||||||
|
err := client.Files.Delete(context.TODO(), 1, 2, 3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("File deleted successfully!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Save File
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := onspring.NewClient("your-api-key")
|
||||||
|
|
||||||
|
file, _ := os.Open("document.pdf")
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 2,
|
||||||
|
FileName: "document.pdf",
|
||||||
|
FileContents: file,
|
||||||
|
Notes: "Uploaded via API",
|
||||||
|
ModifiedDate: "2024-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Files.Save(context.TODO(), saveReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Saved File Id: %d\n", response.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ type Client struct {
|
|||||||
Lists *ListsEndpoint
|
Lists *ListsEndpoint
|
||||||
// Reports provides access to the reports within an Onspring instance.
|
// Reports provides access to the reports within an Onspring instance.
|
||||||
Reports *ReportsEndpoint
|
Reports *ReportsEndpoint
|
||||||
|
// Files provides access to the files within an Onspring instance.
|
||||||
|
Files *FilesEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new Onspring API client with the provided API key.
|
// NewClient creates a new Onspring API client with the provided API key.
|
||||||
@@ -88,6 +90,7 @@ func NewClient(apiKey string, opts ...ClientOption) *Client {
|
|||||||
c.Fields = &FieldsEndpoint{client: c}
|
c.Fields = &FieldsEndpoint{client: c}
|
||||||
c.Lists = &ListsEndpoint{client: c}
|
c.Lists = &ListsEndpoint{client: c}
|
||||||
c.Reports = &ReportsEndpoint{client: c}
|
c.Reports = &ReportsEndpoint{client: c}
|
||||||
|
c.Files = &FilesEndpoint{client: c}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -149,6 +152,80 @@ func (c *Client) doWithJsonResponse(req *http.Request, v any) error {
|
|||||||
return json.NewDecoder(resp.Body).Decode(v)
|
return json.NewDecoder(resp.Body).Decode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doWithBytesResponse executes an HTTP request and returns the raw response bytes and headers.
|
||||||
|
// It performs the HTTP call, checks the response status code, and reads
|
||||||
|
// the response body into a byte slice.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - req: The HTTP request to execute
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - []byte: The raw response body bytes
|
||||||
|
// - http.Header: The response headers
|
||||||
|
// - error: nil if the request succeeds, or an error if the request fails
|
||||||
|
// or returns a non-2xx status code
|
||||||
|
func (c *Client) doWithBytesResponse(req *http.Request) ([]byte, http.Header, error) {
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, nil, c.handleAPIError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, resp.Header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMultipartRequest creates a new multipart/form-data HTTP request for the Onspring API.
|
||||||
|
// It constructs the full URL, sets required authentication headers,
|
||||||
|
// and prepares the multipart form data with the provided context.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: The context for the request
|
||||||
|
// - path: The API endpoint path
|
||||||
|
// - body: The request body as a reader (should be a multipart form body)
|
||||||
|
// - contentType: The content type header value (including boundary)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *http.Request: The prepared HTTP request
|
||||||
|
// - error: An error if the context is nil or request creation fails
|
||||||
|
func (c *Client) newMultipartRequest(ctx context.Context, path string, body io.Reader, contentType string) (*http.Request, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, fmt.Errorf("context must not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL := fmt.Sprintf("%s/%s", strings.TrimRight(c.baseURL, "/"), strings.TrimLeft(path, "/"))
|
||||||
|
validUrl, urlParsingError := url.Parse(fullURL)
|
||||||
|
|
||||||
|
if urlParsingError != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse the request url: %w", urlParsingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, validUrl.String(), body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(defaultAPIKeyHeader, c.apiKey)
|
||||||
|
req.Header.Set(defaultAPIVersionHeader, c.apiVersion)
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleAPIError processes error responses from the Onspring API.
|
// handleAPIError processes error responses from the Onspring API.
|
||||||
// It attempts to decode the error message from the response body.
|
// It attempts to decode the error message from the response body.
|
||||||
// If decoding fails, it falls back to using the HTTP status text.
|
// If decoding fails, it falls back to using the HTTP status text.
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package onspring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
filesPath = "/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilesEndpoint provides access to files in an Onspring instance.
|
||||||
|
type FilesEndpoint struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileInfo represents the metadata of a file in Onspring.
|
||||||
|
type FileInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedDate string `json:"createdDate"`
|
||||||
|
ModifiedDate string `json:"modifiedDate"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
FileHref string `json:"fileHref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileContent represents the content of a downloaded file.
|
||||||
|
type FileContent struct {
|
||||||
|
FileName string
|
||||||
|
ContentType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFileRequest represents a request to upload a file.
|
||||||
|
type SaveFileRequest struct {
|
||||||
|
RecordId int
|
||||||
|
FieldId int
|
||||||
|
Notes string
|
||||||
|
ModifiedDate string
|
||||||
|
FileName string
|
||||||
|
FileContents io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFileResponse represents the response for saving a file.
|
||||||
|
type SaveFileResponse struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileBasePath(recordId, fieldId, fileId int) string {
|
||||||
|
return fmt.Sprintf("%s/recordId/%d/fieldId/%d/fileId/%d", filesPath, recordId, fieldId, fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo retrieves a file's metadata from the Onspring API.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: The context for the request
|
||||||
|
// - recordId: The id of the record
|
||||||
|
// - fieldId: The id of the field
|
||||||
|
// - fileId: The id of the file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - FileInfo: The file's metadata
|
||||||
|
// - error: An error if the request fails
|
||||||
|
func (f *FilesEndpoint) GetInfo(ctx context.Context, recordId, fieldId, fileId int) (FileInfo, error) {
|
||||||
|
path := fileBasePath(recordId, fieldId, fileId)
|
||||||
|
req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, nil, nil)
|
||||||
|
|
||||||
|
var fileInfo FileInfo
|
||||||
|
|
||||||
|
if requestCreationErr != nil {
|
||||||
|
return fileInfo, requestCreationErr
|
||||||
|
}
|
||||||
|
|
||||||
|
responseErr := f.client.doWithJsonResponse(req, &fileInfo)
|
||||||
|
|
||||||
|
if responseErr != nil {
|
||||||
|
return fileInfo, responseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContent retrieves a file's content from the Onspring API.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: The context for the request
|
||||||
|
// - recordId: The id of the record
|
||||||
|
// - fieldId: The id of the field
|
||||||
|
// - fileId: The id of the file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - FileContent: The file's content including name, content type, and data
|
||||||
|
// - error: An error if the request fails
|
||||||
|
func (f *FilesEndpoint) GetContent(ctx context.Context, recordId, fieldId, fileId int) (FileContent, error) {
|
||||||
|
path := fmt.Sprintf("%s/file", fileBasePath(recordId, fieldId, fileId))
|
||||||
|
req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, nil, nil)
|
||||||
|
|
||||||
|
var fileContent FileContent
|
||||||
|
|
||||||
|
if requestCreationErr != nil {
|
||||||
|
return fileContent, requestCreationErr
|
||||||
|
}
|
||||||
|
|
||||||
|
data, headers, responseErr := f.client.doWithBytesResponse(req)
|
||||||
|
|
||||||
|
if responseErr != nil {
|
||||||
|
return fileContent, responseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent.Data = data
|
||||||
|
fileContent.ContentType = headers.Get("Content-Type")
|
||||||
|
|
||||||
|
contentDisposition := headers.Get("Content-Disposition")
|
||||||
|
|
||||||
|
if contentDisposition != "" {
|
||||||
|
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
fileContent.FileName = params["filename"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a file from the Onspring API.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: The context for the request
|
||||||
|
// - recordId: The id of the record
|
||||||
|
// - fieldId: The id of the field
|
||||||
|
// - fileId: The id of the file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: An error if the request fails
|
||||||
|
func (f *FilesEndpoint) Delete(ctx context.Context, recordId, fieldId, fileId int) error {
|
||||||
|
path := fileBasePath(recordId, fieldId, fileId)
|
||||||
|
req, requestCreationErr := f.client.newRequest(ctx, http.MethodDelete, path, nil, nil)
|
||||||
|
|
||||||
|
if requestCreationErr != nil {
|
||||||
|
return requestCreationErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.client.do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save uploads a file to the Onspring API.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: The context for the request
|
||||||
|
// - saveReq: The file upload request containing record id, field id, file name, contents, and optional metadata
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - SaveFileResponse: The response containing the saved file's id
|
||||||
|
// - error: An error if the request fails
|
||||||
|
func (f *FilesEndpoint) Save(ctx context.Context, saveReq SaveFileRequest) (SaveFileResponse, error) {
|
||||||
|
var response SaveFileResponse
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
_ = writer.WriteField("RecordId", strconv.Itoa(saveReq.RecordId))
|
||||||
|
_ = writer.WriteField("FieldId", strconv.Itoa(saveReq.FieldId))
|
||||||
|
|
||||||
|
if saveReq.Notes != "" {
|
||||||
|
_ = writer.WriteField("Notes", saveReq.Notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if saveReq.ModifiedDate != "" {
|
||||||
|
_ = writer.WriteField("ModifiedDate", saveReq.ModifiedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePart, err := writer.CreateFormFile("File", saveReq.FileName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return response, fmt.Errorf("failed to create form file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(filePart, saveReq.FileContents)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return response, fmt.Errorf("failed to copy file contents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writer.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return response, fmt.Errorf("failed to close multipart writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, requestCreationErr := f.client.newMultipartRequest(ctx, filesPath, body, writer.FormDataContentType())
|
||||||
|
|
||||||
|
if requestCreationErr != nil {
|
||||||
|
return response, requestCreationErr
|
||||||
|
}
|
||||||
|
|
||||||
|
responseErr := f.client.doWithJsonResponse(req, &response)
|
||||||
|
|
||||||
|
if responseErr != nil {
|
||||||
|
return response, responseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
+575
@@ -0,0 +1,575 @@
|
|||||||
|
package onspring_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/onspring-api-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFiles(t *testing.T) {
|
||||||
|
t.Run("GetInfo", 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.Files.GetInfo(nilContext, 1, 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.Files.GetInfo(ctx, 1, 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.Files.GetInfo(t.Context(), 1, 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.Files.GetInfo(t.Context(), 1, 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.Files.GetInfo(t.Context(), 1, 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 file info", func(t *testing.T) {
|
||||||
|
recordId := 1
|
||||||
|
fieldId := 2
|
||||||
|
fileId := 3
|
||||||
|
|
||||||
|
expectedFileInfo := onspring.FileInfo{
|
||||||
|
Type: "Attachment",
|
||||||
|
ContentType: "application/pdf",
|
||||||
|
Name: "test.pdf",
|
||||||
|
CreatedDate: "2024-01-01T00:00:00Z",
|
||||||
|
ModifiedDate: "2024-01-02T00:00:00Z",
|
||||||
|
Owner: "admin",
|
||||||
|
Notes: "Test notes",
|
||||||
|
FileHref: "https://api.onspring.com/files/1/2/3/file",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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("/files/recordId/%d/fieldId/%d/fileId/%d", recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if r.URL.Path != expectedPath {
|
||||||
|
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.Marshal(expectedFileInfo)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(jsonData)
|
||||||
|
})
|
||||||
|
|
||||||
|
fileInfo, err := client.Files.GetInfo(t.Context(), recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expectedFileInfo, fileInfo) {
|
||||||
|
t.Errorf("Expected %v but got %v", expectedFileInfo, fileInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetContent", 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.Files.GetContent(nilContext, 1, 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.Files.GetContent(ctx, 1, 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.Files.GetContent(t.Context(), 1, 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.Files.GetContent(t.Context(), 1, 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.Files.GetContent(t.Context(), 1, 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 file content", func(t *testing.T) {
|
||||||
|
recordId := 1
|
||||||
|
fieldId := 2
|
||||||
|
fileId := 3
|
||||||
|
expectedData := []byte("file content here")
|
||||||
|
expectedContentType := "application/pdf"
|
||||||
|
expectedFileName := "test.pdf"
|
||||||
|
|
||||||
|
_, 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("/files/recordId/%d/fieldId/%d/fileId/%d/file", recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if r.URL.Path != expectedPath {
|
||||||
|
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", expectedContentType)
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, expectedFileName))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(expectedData)
|
||||||
|
})
|
||||||
|
|
||||||
|
fileContent, err := client.Files.GetContent(t.Context(), recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileContent.FileName != expectedFileName {
|
||||||
|
t.Errorf("Expected file name %s but got %s", expectedFileName, fileContent.FileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileContent.ContentType != expectedContentType {
|
||||||
|
t.Errorf("Expected content type %s but got %s", expectedContentType, fileContent.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(expectedData, fileContent.Data) {
|
||||||
|
t.Errorf("Expected data %v but got %v", expectedData, fileContent.Data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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.Files.Delete(nilContext, 1, 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.Files.Delete(ctx, 1, 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.Files.Delete(t.Context(), 1, 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.Files.Delete(t.Context(), 1, 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.Files.Delete(t.Context(), 1, 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) {
|
||||||
|
recordId := 1
|
||||||
|
fieldId := 2
|
||||||
|
fileId := 3
|
||||||
|
|
||||||
|
_, 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("/files/recordId/%d/fieldId/%d/fileId/%d", recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if r.URL.Path != expectedPath {
|
||||||
|
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
err := client.Files.Delete(t.Context(), recordId, fieldId, fileId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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.StatusCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
var nilContext context.Context = nil
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 1,
|
||||||
|
FileName: "test.pdf",
|
||||||
|
FileContents: strings.NewReader("file content"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Files.Save(nilContext, saveReq)
|
||||||
|
|
||||||
|
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.StatusCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 1,
|
||||||
|
FileName: "test.pdf",
|
||||||
|
FileContents: strings.NewReader("file content"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Files.Save(ctx, saveReq)
|
||||||
|
|
||||||
|
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{}}),
|
||||||
|
)
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 1,
|
||||||
|
FileName: "test.pdf",
|
||||||
|
FileContents: strings.NewReader("file content"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Files.Save(t.Context(), saveReq)
|
||||||
|
|
||||||
|
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.StatusCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
invalidClient := onspring.NewClient(
|
||||||
|
"test-api-key",
|
||||||
|
onspring.WithBaseURL("http://[::1]:namedport"),
|
||||||
|
onspring.WithHTTPClient(client.HTTPClient()),
|
||||||
|
)
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 1,
|
||||||
|
FileName: "test.pdf",
|
||||||
|
FileContents: strings.NewReader("file content"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := invalidClient.Files.Save(t.Context(), saveReq)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: 1,
|
||||||
|
FieldId: 1,
|
||||||
|
FileName: "test.pdf",
|
||||||
|
FileContents: strings.NewReader("file content"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Files.Save(t.Context(), saveReq)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it should perform a POST request to the /files endpoint with multipart form data and return a response when successful", func(t *testing.T) {
|
||||||
|
expectedRecordId := 1
|
||||||
|
expectedFieldId := 2
|
||||||
|
expectedNotes := "Test notes"
|
||||||
|
expectedModifiedDate := "2024-01-01T00:00:00Z"
|
||||||
|
expectedFileName := "test.pdf"
|
||||||
|
expectedFileContent := "file content here"
|
||||||
|
|
||||||
|
expectedResponse := onspring.SaveFileResponse{
|
||||||
|
Id: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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 != "/files" {
|
||||||
|
t.Errorf("Expected /files endpoint, got %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if !strings.HasPrefix(contentType, "multipart/form-data") {
|
||||||
|
t.Errorf("Expected multipart/form-data content type, got %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.ParseMultipartForm(10 << 20)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected to parse multipart form, but got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recordId := r.FormValue("RecordId")
|
||||||
|
|
||||||
|
if recordId != fmt.Sprintf("%d", expectedRecordId) {
|
||||||
|
t.Errorf("Expected RecordId %d but got %s", expectedRecordId, recordId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldId := r.FormValue("FieldId")
|
||||||
|
|
||||||
|
if fieldId != fmt.Sprintf("%d", expectedFieldId) {
|
||||||
|
t.Errorf("Expected FieldId %d but got %s", expectedFieldId, fieldId)
|
||||||
|
}
|
||||||
|
|
||||||
|
notes := r.FormValue("Notes")
|
||||||
|
|
||||||
|
if notes != expectedNotes {
|
||||||
|
t.Errorf("Expected Notes %s but got %s", expectedNotes, notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedDate := r.FormValue("ModifiedDate")
|
||||||
|
|
||||||
|
if modifiedDate != expectedModifiedDate {
|
||||||
|
t.Errorf("Expected ModifiedDate %s but got %s", expectedModifiedDate, modifiedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, header, err := r.FormFile("File")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected to get file from form, but got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if header.Filename != expectedFileName {
|
||||||
|
t.Errorf("Expected file name %s but got %s", expectedFileName, header.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, _ := io.ReadAll(file)
|
||||||
|
|
||||||
|
if string(fileBytes) != expectedFileContent {
|
||||||
|
t.Errorf("Expected file content %s but got %s", expectedFileContent, string(fileBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.Marshal(expectedResponse)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(jsonData)
|
||||||
|
})
|
||||||
|
|
||||||
|
saveReq := onspring.SaveFileRequest{
|
||||||
|
RecordId: expectedRecordId,
|
||||||
|
FieldId: expectedFieldId,
|
||||||
|
Notes: expectedNotes,
|
||||||
|
ModifiedDate: expectedModifiedDate,
|
||||||
|
FileName: expectedFileName,
|
||||||
|
FileContents: strings.NewReader(expectedFileContent),
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Files.Save(t.Context(), saveReq)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user