Merge pull request #6 from StevanFreeborn/stevanfreeborn/feat/add-files-endpoints

feat: implement files endpoint
This commit is contained in:
Stevan Freeborn
2026-03-26 12:56:15 -05:00
committed by GitHub
4 changed files with 963 additions and 0 deletions
+99
View File
@@ -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)
}
+77
View File
@@ -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.
+212
View File
@@ -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
View File
@@ -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)
}
})
})
}