diff --git a/.gitignore b/.gitignore index a1c1a2c..df4f4c3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ go.work.sum *.out *.html cover.out -coverage.txt \ No newline at end of file +coverage.txt +.env \ No newline at end of file diff --git a/apps_integration_test.go b/apps_integration_test.go new file mode 100644 index 0000000..f8444b7 --- /dev/null +++ b/apps_integration_test.go @@ -0,0 +1,211 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestAppsIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("Get", func(t *testing.T) { + t.Run("should return an app", func(t *testing.T) { + appId := requireEnvInt(t, "TEST_APP_ID") + + app, err := client.Apps.Get(ctx, appId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if app.Id != appId { + t.Errorf("Expected app id %d, got %d", appId, app.Id) + } + + if app.Name == "" { + t.Error("Expected app name to not be empty") + } + + if app.Href == "" { + t.Error("Expected app href to not be empty") + } + }) + + t.Run("should return a 401 error when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + appId := requireEnvInt(t, "TEST_APP_ID") + + _, err := invalidClient.Apps.Get(ctx, appId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when api key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + + _, err := client.Apps.Get(ctx, appIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when an app id cannot be found", func(t *testing.T) { + _, err := client.Apps.Get(ctx, 0) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("List", func(t *testing.T) { + t.Run("should return a paged list of apps", func(t *testing.T) { + page, err := client.Apps.List(ctx) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber == 0 { + t.Error("Expected page number to not be zero") + } + + if page.PageSize == 0 { + t.Error("Expected page size to not be zero") + } + + if page.TotalPages == 0 { + t.Error("Expected total pages to not be zero") + } + + if page.TotalRecords == 0 { + t.Error("Expected total records to not be zero") + } + + if len(page.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, app := range page.Items { + if app.Id == 0 { + t.Error("Expected app id to not be zero") + } + + if app.Name == "" { + t.Error("Expected app name to not be empty") + } + + if app.Href == "" { + t.Error("Expected app href to not be empty") + } + } + }) + + t.Run("should return a paged list of apps with correct page size and number when passed paging request", func(t *testing.T) { + page, err := client.Apps.List(ctx, onspring.ForPageNumber(1), onspring.WithPageSize(1)) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber != 1 { + t.Errorf("Expected page number 1, got %d", page.PageNumber) + } + + if page.PageSize != 1 { + t.Errorf("Expected page size 1, got %d", page.PageSize) + } + + if len(page.Items) != 1 { + t.Errorf("Expected 1 item, got %d", len(page.Items)) + } + }) + + t.Run("should return a 400 response when an invalid page size is used", func(t *testing.T) { + _, err := client.Apps.List(ctx, onspring.WithPageSize(1001)) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + + _, err := invalidClient.Apps.List(ctx) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + }) + + t.Run("ListAll", func(t *testing.T) { + t.Run("should iterate all apps", func(t *testing.T) { + var apps []onspring.App + + for app, err := range client.Apps.ListAll(ctx, onspring.WithPageSize(1)) { + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + apps = append(apps, app) + } + + if len(apps) == 0 { + t.Error("Expected to iterate at least one app") + } + }) + }) + + t.Run("GetMany", func(t *testing.T) { + t.Run("should return a collection of apps", func(t *testing.T) { + appIds := requireEnvIntSlice(t, "TEST_APP_IDS") + + batch, err := client.Apps.GetMany(ctx, appIds) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if batch.Count != len(appIds) { + t.Errorf("Expected count %d, got %d", len(appIds), batch.Count) + } + + if len(batch.Items) != len(appIds) { + t.Errorf("Expected %d items, got %d", len(appIds), len(batch.Items)) + } + + for _, app := range batch.Items { + if app.Id == 0 { + t.Error("Expected app id to not be zero") + } + + if app.Name == "" { + t.Error("Expected app name to not be empty") + } + + if app.Href == "" { + t.Error("Expected app href to not be empty") + } + } + }) + + t.Run("should return a 401 error when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + appIds := requireEnvIntSlice(t, "TEST_APP_IDS") + + _, err := invalidClient.Apps.GetMany(ctx, appIds) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when api key does not have access to the apps", func(t *testing.T) { + appIdsNoAccess := requireEnvIntSlice(t, "TEST_APP_IDS_NO_ACCESS") + + _, err := client.Apps.GetMany(ctx, appIdsNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) +} diff --git a/client.go b/client.go index 317961d..bae4bdf 100644 --- a/client.go +++ b/client.go @@ -21,7 +21,7 @@ const ( // defaultTimeout is the default HTTP client timeout duration. defaultTimeout = 120 * time.Second // defaultAPIKeyHeader is the HTTP header name used for API key authentication. - defaultAPIKeyHeader = "x-api-key" + defaultAPIKeyHeader = "x-apikey" // defaultAPIVersionHeader is the HTTP header name used to specify the API version. defaultAPIVersionHeader = "x-api-version" // defaultAPIVersion is the default Onspring API version to use. diff --git a/example.env b/example.env new file mode 100644 index 0000000..c41e461 --- /dev/null +++ b/example.env @@ -0,0 +1,35 @@ +API_BASE_URL=https://api.onspring.com +SANDBOX_API_KEY=your-sandbox-api-key + +TEST_APP_ID=1 +TEST_APP_ID_NO_ACCESS=2 +TEST_APP_IDS=1,2,3 +TEST_APP_IDS_NO_ACCESS=4,5,6 + +TEST_SURVEY_ID=1 +TEST_SURVEY_RECORD_ID=1 +TEST_SURVEY_AUTO_NUMBER_FIELD=1 + +TEST_FIELD_ID=1 +TEST_FIELD_ID_NO_ACCESS=2 +TEST_FIELD_IDS=1,2,3 +TEST_FIELD_IDS_NO_ACCESS=4,5,6 +TEST_TEXT_FIELD=1 + +TEST_RECORD=1 +TEST_ATTACHMENT_FIELD=1 +TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD=2 +TEST_ATTACHMENT_FIELD_NO_ACCESS_APP=3 +TEST_ATTACHMENT=1 +TEST_IMAGE_FIELD=1 +TEST_IMAGE=1 + +TEST_LIST_FIELD=1 +TEST_LIST_FIELD_NO_ACCESS=2 +TEST_LIST_ID=1 +TEST_LIST_ID_NO_ACCESS=2 +TEST_LIST_ITEM_ID_NO_ACCESS=some-guid + +TEST_REPORT=1 +TEST_REPORT_NO_ACCESS=2 +TEST_REPORT_WITH_CHART_DATA=3 diff --git a/fields.go b/fields.go index 112a125..4228802 100644 --- a/fields.go +++ b/fields.go @@ -31,8 +31,8 @@ type Field struct { // FormulaField represents a formula field type in Onspring type FormulaField struct { - OutputType string `json:"outputType"` - Values []string `json:"values"` + OutputType string `json:"outputType"` + Values []ListValue `json:"values"` } // ReferenceField represents a reference field type in Onspring @@ -41,11 +41,20 @@ type ReferenceField struct { ReferenceAppId string `json:"referenceAppId"` } +// ListValue represents a value option within a list field in Onspring. +type ListValue struct { + Id string `json:"id"` + Name string `json:"name"` + SortOrder int `json:"sortOrder"` + NumericValue float64 `json:"numericValue"` + Color string `json:"color"` +} + // ListField represents a list field type in Onspring type ListField struct { - Multiplicity string `json:"multiplicity"` - Values []string `json:"values"` - ListId int `json:"listId"` + Multiplicity string `json:"multiplicity"` + Values []ListValue `json:"values"` + ListId int `json:"listId"` } // UnmarshalJSON implements the json.Unmarshaler interface for Field. diff --git a/fields_integration_test.go b/fields_integration_test.go new file mode 100644 index 0000000..8eea075 --- /dev/null +++ b/fields_integration_test.go @@ -0,0 +1,232 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestFieldsIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("Get", func(t *testing.T) { + t.Run("should return a field", func(t *testing.T) { + fieldId := requireEnvInt(t, "TEST_FIELD_ID") + + field, err := client.Fields.Get(ctx, fieldId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if field.Id != fieldId { + t.Errorf("Expected field id %d, got %d", fieldId, field.Id) + } + + if field.Name == "" { + t.Error("Expected field name to not be empty") + } + + if field.AppId == 0 { + t.Error("Expected field appId to not be zero") + } + + if field.Type == "" { + t.Error("Expected field type to not be empty") + } + + if field.Status == "" { + t.Error("Expected field status to not be empty") + } + }) + + t.Run("should return a 401 error when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + fieldId := requireEnvInt(t, "TEST_FIELD_ID") + + _, err := invalidClient.Fields.Get(ctx, fieldId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when api key does not have access to the field", func(t *testing.T) { + fieldIdNoAccess := requireEnvInt(t, "TEST_FIELD_ID_NO_ACCESS") + + _, err := client.Fields.Get(ctx, fieldIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when the field does not exist", func(t *testing.T) { + _, err := client.Fields.Get(ctx, 0) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("List", func(t *testing.T) { + t.Run("should return a paged list of fields", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + page, err := client.Fields.List(ctx, surveyId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber == 0 { + t.Error("Expected page number to not be zero") + } + + if page.TotalRecords == 0 { + t.Error("Expected total records to not be zero") + } + + if len(page.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, field := range page.Items { + if field.Id == 0 { + t.Error("Expected field id to not be zero") + } + + if field.Name == "" { + t.Error("Expected field name to not be empty") + } + + if field.AppId == 0 { + t.Error("Expected field appId to not be zero") + } + + if field.Type == "" { + t.Error("Expected field type to not be empty") + } + + if field.Status == "" { + t.Error("Expected field status to not be empty") + } + } + }) + + t.Run("should return a paged list of fields with correct page size and number when passed paging request", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + page, err := client.Fields.List(ctx, surveyId, onspring.ForPageNumber(1), onspring.WithPageSize(1)) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber != 1 { + t.Errorf("Expected page number 1, got %d", page.PageNumber) + } + + if page.PageSize != 1 { + t.Errorf("Expected page size 1, got %d", page.PageSize) + } + + if len(page.Items) != 1 { + t.Errorf("Expected 1 item, got %d", len(page.Items)) + } + }) + + t.Run("should return a 400 response when an invalid page size is used", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + _, err := client.Fields.List(ctx, surveyId, onspring.WithPageSize(1001)) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + _, err := invalidClient.Fields.List(ctx, surveyId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when api key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + + _, err := client.Fields.List(ctx, appIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) + + t.Run("ListAll", func(t *testing.T) { + t.Run("should iterate all fields for an app", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + var fields []onspring.Field + + for field, err := range client.Fields.ListAll(ctx, surveyId, onspring.WithPageSize(1)) { + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + fields = append(fields, field) + } + + if len(fields) == 0 { + t.Error("Expected to iterate at least one field") + } + }) + }) + + t.Run("GetMany", func(t *testing.T) { + t.Run("should return a collection of fields", func(t *testing.T) { + fieldIds := requireEnvIntSlice(t, "TEST_FIELD_IDS") + + batch, err := client.Fields.GetMany(ctx, fieldIds) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if batch.Count != len(fieldIds) { + t.Errorf("Expected count %d, got %d", len(fieldIds), batch.Count) + } + + if len(batch.Items) != len(fieldIds) { + t.Errorf("Expected %d items, got %d", len(fieldIds), len(batch.Items)) + } + + for _, field := range batch.Items { + if field.Id == 0 { + t.Error("Expected field id to not be zero") + } + + if field.Name == "" { + t.Error("Expected field name to not be empty") + } + } + }) + + t.Run("should return a 401 response when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + fieldIds := requireEnvIntSlice(t, "TEST_FIELD_IDS") + + _, err := invalidClient.Fields.GetMany(ctx, fieldIds) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when the api key does not have access to any of the requested fields", func(t *testing.T) { + fieldIdsNoAccess := requireEnvIntSlice(t, "TEST_FIELD_IDS_NO_ACCESS") + + _, err := client.Fields.GetMany(ctx, fieldIdsNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) +} diff --git a/fields_test.go b/fields_test.go index 52bbb0f..3af58cd 100644 --- a/fields_test.go +++ b/fields_test.go @@ -138,7 +138,10 @@ func TestFields(t *testing.T) { "isRequired": true, "isUnique": false, "outputType": "Number", - "values": ["1", "2", "3"] + "values": [ + {"id": "a1", "name": "Value1", "sortOrder": 1, "numericValue": 1, "color": "#fff"}, + {"id": "b2", "name": "Value2", "sortOrder": 2, "numericValue": 2, "color": "#000"} + ] }` var field onspring.Field @@ -161,7 +164,10 @@ func TestFields(t *testing.T) { expectedFormulaField := onspring.FormulaField{ OutputType: "Number", - Values: []string{"1", "2", "3"}, + Values: []onspring.ListValue{ + {Id: "a1", Name: "Value1", SortOrder: 1, NumericValue: 1, Color: "#fff"}, + {Id: "b2", Name: "Value2", SortOrder: 2, NumericValue: 2, Color: "#000"}, + }, } if !reflect.DeepEqual(formulaField, expectedFormulaField) { @@ -220,7 +226,10 @@ func TestFields(t *testing.T) { "isRequired": false, "isUnique": false, "multiplicity": "MultiSelect", - "values": ["OptionA", "OptionB"], + "values": [ + {"id": "aaa", "name": "OptionA", "sortOrder": 1, "numericValue": 0, "color": "#ffffff"}, + {"id": "bbb", "name": "OptionB", "sortOrder": 2, "numericValue": 0, "color": "#000000"} + ], "listId": 456 }` @@ -244,8 +253,11 @@ func TestFields(t *testing.T) { expectedListField := onspring.ListField{ Multiplicity: "MultiSelect", - Values: []string{"OptionA", "OptionB"}, - ListId: 456, + Values: []onspring.ListValue{ + {Id: "aaa", Name: "OptionA", SortOrder: 1, NumericValue: 0, Color: "#ffffff"}, + {Id: "bbb", Name: "OptionB", SortOrder: 2, NumericValue: 0, Color: "#000000"}, + }, + ListId: 456, } if !reflect.DeepEqual(listField, expectedListField) { diff --git a/files_integration_test.go b/files_integration_test.go new file mode 100644 index 0000000..1b552ce --- /dev/null +++ b/files_integration_test.go @@ -0,0 +1,590 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "net/http" + "os" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestFilesIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("GetInfo", func(t *testing.T) { + t.Run("should return information about a file in an attachment field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + info, err := client.Files.GetInfo(ctx, recordId, fieldId, fileId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if info.Name == "" { + t.Error("Expected name to not be empty") + } + + if info.ContentType == "" { + t.Error("Expected content type to not be empty") + } + + if info.CreatedDate == "" { + t.Error("Expected created date to not be empty") + } + + if info.ModifiedDate == "" { + t.Error("Expected modified date to not be empty") + } + + if info.Owner == "" { + t.Error("Expected owner to not be empty") + } + + if info.Type == "" { + t.Error("Expected type to not be empty") + } + + if info.FileHref == "" { + t.Error("Expected file href to not be empty") + } + }) + + t.Run("should return information about a file in an image field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_IMAGE_FIELD") + fileId := requireEnvInt(t, "TEST_IMAGE") + + info, err := client.Files.GetInfo(ctx, recordId, fieldId, fileId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if info.Name == "" { + t.Error("Expected name to not be empty") + } + + if info.ContentType == "" { + t.Error("Expected content type to not be empty") + } + + if info.Type == "" { + t.Error("Expected type to not be empty") + } + }) + + t.Run("should return a 400 response when fieldId is not for a file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetInfo(ctx, recordId, textFieldId, fileId) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := invalidClient.Files.GetInfo(ctx, recordId, fieldId, fileId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when the api key does not have access to the file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoAccess := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetInfo(ctx, recordId, fieldIdNoAccess, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 403 response when the api key does not have access to the app", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoApp := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_APP") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetInfo(ctx, recordId, fieldIdNoApp, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 response when the file field cannot be found", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetInfo(ctx, recordId, 0, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + + t.Run("should return a 404 response when the file record cannot be found", func(t *testing.T) { + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetInfo(ctx, 0, fieldId, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("GetContent", func(t *testing.T) { + t.Run("should return a file in an attachment field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + content, err := client.Files.GetContent(ctx, recordId, fieldId, fileId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(content.Data) == 0 { + t.Error("Expected data to not be empty") + } + + if content.ContentType == "" { + t.Error("Expected content type to not be empty") + } + + if content.FileName == "" { + t.Error("Expected file name to not be empty") + } + }) + + t.Run("should return a file in an image field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_IMAGE_FIELD") + fileId := requireEnvInt(t, "TEST_IMAGE") + + content, err := client.Files.GetContent(ctx, recordId, fieldId, fileId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(content.Data) == 0 { + t.Error("Expected data to not be empty") + } + + if content.ContentType == "" { + t.Error("Expected content type to not be empty") + } + + if content.FileName == "" { + t.Error("Expected file name to not be empty") + } + }) + + t.Run("should return a 400 response when fieldId is not for a file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetContent(ctx, recordId, textFieldId, fileId) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := invalidClient.Files.GetContent(ctx, recordId, fieldId, fileId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when the api key does not have access to the file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoAccess := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetContent(ctx, recordId, fieldIdNoAccess, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 403 response when the api key does not have access to the app", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoApp := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_APP") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetContent(ctx, recordId, fieldIdNoApp, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 response when the file field cannot be found", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetContent(ctx, recordId, 0, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + + t.Run("should return a 404 response when the file record cannot be found", func(t *testing.T) { + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + _, err := client.Files.GetContent(ctx, 0, fieldId, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("Save", func(t *testing.T) { + t.Run("should save a file into an attachment field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + response, err := client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldId, + FileName: "test-attachment.txt", + FileContents: file, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if response.Id == 0 { + t.Error("Expected file id to not be zero") + } + + t.Cleanup(func() { + _ = client.Files.Delete(context.Background(), recordId, fieldId, response.Id) + }) + }) + + t.Run("should save a file into an image field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_IMAGE_FIELD") + + file, err := os.Open("testdata/test-image.jpeg") + + if err != nil { + t.Fatalf("Failed to open test image: %v", err) + } + + defer func() { + _ = file.Close() + }() + + response, err := client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldId, + FileName: "test-image.jpeg", + FileContents: file, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if response.Id == 0 { + t.Error("Expected file id to not be zero") + } + + t.Cleanup(func() { + _ = client.Files.Delete(context.Background(), recordId, fieldId, response.Id) + }) + }) + + t.Run("should return a 400 response when fieldId is not for a file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: textFieldId, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = invalidClient.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldId, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when the api key does not have access to the field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoAccess := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldIdNoAccess, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 403 response when the api key does not have access to the app", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoApp := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_APP") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldIdNoApp, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 response when the file field cannot be found", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: 0, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusNotFound) + }) + + t.Run("should return a 404 response when the file record cannot be found", func(t *testing.T) { + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + _, err = client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: 0, + FieldId: fieldId, + FileName: "test-attachment.txt", + FileContents: file, + }) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("should delete a file from an attachment field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + + file, err := os.Open("testdata/test-attachment.txt") + + if err != nil { + t.Fatalf("Failed to open test attachment: %v", err) + } + + defer func() { + _ = file.Close() + }() + + saveResponse, err := client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldId, + FileName: "test-attachment-to-delete.txt", + FileContents: file, + }) + + if err != nil { + t.Fatalf("Failed to save file: %v", err) + } + + err = client.Files.Delete(ctx, recordId, fieldId, saveResponse.Id) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("should delete a file from an image field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_IMAGE_FIELD") + + file, err := os.Open("testdata/test-image.jpeg") + + if err != nil { + t.Fatalf("Failed to open test image: %v", err) + } + + defer func() { + _ = file.Close() + }() + + saveResponse, err := client.Files.Save(ctx, onspring.SaveFileRequest{ + RecordId: recordId, + FieldId: fieldId, + FileName: "test-image-to-delete.jpeg", + FileContents: file, + }) + + if err != nil { + t.Fatalf("Failed to save file: %v", err) + } + + err = client.Files.Delete(ctx, recordId, fieldId, saveResponse.Id) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("should return a 400 response when fieldId is not for a file field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := client.Files.Delete(ctx, recordId, textFieldId, fileId) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 response when the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + recordId := requireEnvInt(t, "TEST_RECORD") + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := invalidClient.Files.Delete(ctx, recordId, fieldId, fileId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 response when the api key does not have access to the field", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoAccess := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := client.Files.Delete(ctx, recordId, fieldIdNoAccess, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 403 response when the api key does not have access to the app", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fieldIdNoApp := requireEnvInt(t, "TEST_ATTACHMENT_FIELD_NO_ACCESS_APP") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := client.Files.Delete(ctx, recordId, fieldIdNoApp, fileId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 response when the file field cannot be found", func(t *testing.T) { + recordId := requireEnvInt(t, "TEST_RECORD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := client.Files.Delete(ctx, recordId, 0, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + + t.Run("should return a 404 response when the file record cannot be found", func(t *testing.T) { + fieldId := requireEnvInt(t, "TEST_ATTACHMENT_FIELD") + fileId := requireEnvInt(t, "TEST_ATTACHMENT") + + err := client.Files.Delete(ctx, 0, fieldId, fileId) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) +} diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..81ae876 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,158 @@ +//go:build integration + +package onspring_test + +import ( + "bufio" + "context" + "errors" + "fmt" + "os" + "strconv" + "strings" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func loadEnvFile(t *testing.T) { + t.Helper() + + file, err := os.Open(".env") + if err != nil { + t.Fatalf("Failed to open .env file: %v", err) + } + + defer func() { + _ = file.Close() + }() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + key, value, found := strings.Cut(line, "=") + + if !found { + continue + } + + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + + t.Setenv(key, value) + } + + if err := scanner.Err(); err != nil { + t.Fatalf("Failed to read .env file: %v", err) + } +} + +func requireEnv(t *testing.T, key string) string { + t.Helper() + + value := os.Getenv(key) + + if value == "" { + t.Fatalf("Required environment variable %s is not set", key) + } + + return value +} + +func requireEnvInt(t *testing.T, key string) int { + t.Helper() + + value := requireEnv(t, key) + intValue, err := strconv.Atoi(value) + + if err != nil { + t.Fatalf("Environment variable %s is not a valid integer: %v", key, err) + } + + return intValue +} + +func requireEnvIntSlice(t *testing.T, key string) []int { + t.Helper() + + value := requireEnv(t, key) + parts := strings.Split(value, ",") + result := make([]int, len(parts)) + + for i, part := range parts { + intValue, err := strconv.Atoi(strings.TrimSpace(part)) + + if err != nil { + t.Fatalf("Environment variable %s contains invalid integer '%s': %v", key, part, err) + } + + result[i] = intValue + } + + return result +} + +func createClient(t *testing.T) *onspring.Client { + t.Helper() + + return onspring.NewClient( + requireEnv(t, "SANDBOX_API_KEY"), + onspring.WithBaseURL(requireEnv(t, "API_BASE_URL")), + ) +} + +func createInvalidClient(t *testing.T) *onspring.Client { + t.Helper() + + return onspring.NewClient( + "invalid-api-key", + onspring.WithBaseURL(requireEnv(t, "API_BASE_URL")), + ) +} + +func assertAPIError(t *testing.T, err error, expectedStatus int) { + t.Helper() + + if err == nil { + t.Fatalf("Expected error, got nil") + } + + var apiErr *onspring.OnspringAPIError + + if !errors.As(err, &apiErr) { + t.Fatalf("Expected OnspringAPIError, got %T: %v", err, err) + } + + if apiErr.StatusCode != expectedStatus { + t.Errorf("Expected status code %d, got %d", expectedStatus, apiErr.StatusCode) + } +} + +func addTestRecord(t *testing.T, client *onspring.Client, appId, textFieldId int) int { + t.Helper() + + ctx := context.Background() + + response, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: appId, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "test", + }, + }) + + if err != nil { + t.Fatalf("Failed to create test record: %v", err) + } + + t.Cleanup(func() { + _ = client.Records.Delete(context.Background(), appId, response.Id) + }) + + return response.Id +} diff --git a/lists_integration_test.go b/lists_integration_test.go new file mode 100644 index 0000000..3e252e4 --- /dev/null +++ b/lists_integration_test.go @@ -0,0 +1,155 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestListsIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("Save", func(t *testing.T) { + t.Run("should add a list item", func(t *testing.T) { + listId := requireEnvInt(t, "TEST_LIST_ID") + + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item", + } + + response, err := client.Lists.Save(ctx, listId, item) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if response.Id == "" { + t.Error("Expected list item id to not be empty") + } + + t.Cleanup(func() { + _ = client.Lists.Delete(context.Background(), listId, response.Id) + }) + }) + + t.Run("should update a list item", func(t *testing.T) { + listId := requireEnvInt(t, "TEST_LIST_ID") + + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item", + } + + addResponse, err := client.Lists.Save(ctx, listId, item) + + if err != nil { + t.Fatalf("Failed to add list item: %v", err) + } + + t.Cleanup(func() { + _ = client.Lists.Delete(context.Background(), listId, addResponse.Id) + }) + + updateItem := onspring.SaveListItemRequest{ + Id: addResponse.Id, + Name: "Updated Integration Test Item", + } + + updateResponse, err := client.Lists.Save(ctx, listId, updateItem) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if updateResponse.Id == "" { + t.Error("Expected list item id to not be empty") + } + }) + + t.Run("should return a 401 error when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + listId := requireEnvInt(t, "TEST_LIST_ID") + + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item", + } + + _, err := invalidClient.Lists.Save(ctx, listId, item) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when api key does not have access to the list", func(t *testing.T) { + listIdNoAccess := requireEnvInt(t, "TEST_LIST_ID_NO_ACCESS") + + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item", + } + + _, err := client.Lists.Save(ctx, listIdNoAccess, item) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when the list does not exist", func(t *testing.T) { + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item", + } + + _, err := client.Lists.Save(ctx, 0, item) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("should delete a list item", func(t *testing.T) { + listId := requireEnvInt(t, "TEST_LIST_ID") + + item := onspring.SaveListItemRequest{ + Name: "Integration Test Item To Delete", + } + + response, err := client.Lists.Save(ctx, listId, item) + + if err != nil { + t.Fatalf("Failed to add list item: %v", err) + } + + err = client.Lists.Delete(ctx, listId, response.Id) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("should return a 401 error when an invalid api key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + listId := requireEnvInt(t, "TEST_LIST_ID") + + err := invalidClient.Lists.Delete(ctx, listId, "some-item-id") + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when api key does not have access to the list", func(t *testing.T) { + listIdNoAccess := requireEnvInt(t, "TEST_LIST_ID_NO_ACCESS") + listItemIdNoAccess := requireEnv(t, "TEST_LIST_ITEM_ID_NO_ACCESS") + + err := client.Lists.Delete(ctx, listIdNoAccess, listItemIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when the list does not exist", func(t *testing.T) { + err := client.Lists.Delete(ctx, 0, "a57e3d33-9195-4039-9ac0-c180013b043e") + + assertAPIError(t, err, http.StatusNotFound) + }) + }) +} diff --git a/ping_integration_test.go b/ping_integration_test.go new file mode 100644 index 0000000..d125170 --- /dev/null +++ b/ping_integration_test.go @@ -0,0 +1,22 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "testing" +) + +func TestPingIntegration(t *testing.T) { + loadEnvFile(t) + + t.Run("Get should be able to connect to the API", func(t *testing.T) { + client := createClient(t) + + err := client.Ping.Get(context.Background()) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) +} diff --git a/records_integration_test.go b/records_integration_test.go new file mode 100644 index 0000000..f3c8832 --- /dev/null +++ b/records_integration_test.go @@ -0,0 +1,649 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestRecordsIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("Get", func(t *testing.T) { + t.Run("should get a record", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + record, err := client.Records.Get(ctx, surveyId, recordId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if record.AppId != surveyId { + t.Errorf("Expected appId %d, got %d", surveyId, record.AppId) + } + + if record.RecordId != recordId { + t.Errorf("Expected recordId %d, got %d", recordId, record.RecordId) + } + + if len(record.FieldData) == 0 { + t.Error("Expected field data to not be empty") + } + + for _, field := range record.FieldData { + if field.FieldId == 0 { + t.Error("Expected field id to not be zero") + } + + if field.Type == "" { + t.Error("Expected field type to not be empty") + } + } + }) + + t.Run("should get a record when fieldIds and data format are passed as parameters", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + record, err := client.Records.Get( + ctx, + surveyId, + recordId, + onspring.WithFieldIds([]int{textFieldId}), + onspring.WithRecordDataFormat("Formatted"), + ) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if record.AppId != surveyId { + t.Errorf("Expected appId %d, got %d", surveyId, record.AppId) + } + + if record.RecordId != recordId { + t.Errorf("Expected recordId %d, got %d", recordId, record.RecordId) + } + + if len(record.FieldData) == 0 { + t.Error("Expected field data to not be empty") + } + }) + + t.Run("should return a 401 error when an invalid API key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + _, err := invalidClient.Records.Get(ctx, surveyId, recordId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 404 error when an invalid record id is used", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + _, err := client.Records.Get(ctx, surveyId, 0) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("List", func(t *testing.T) { + t.Run("should get records", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + page, err := client.Records.List(ctx, surveyId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber == 0 { + t.Error("Expected page number to not be zero") + } + + if page.TotalRecords == 0 { + t.Error("Expected total records to not be zero") + } + + if len(page.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, record := range page.Items { + if record.AppId != surveyId { + t.Errorf("Expected appId %d, got %d", surveyId, record.AppId) + } + + if record.RecordId == 0 { + t.Error("Expected recordId to not be zero") + } + } + }) + + t.Run("should get records when fieldIds, paging information, and data format are passed as parameters", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + page, err := client.Records.List( + ctx, + surveyId, + onspring.WithFieldIds([]int{textFieldId}), + onspring.WithRecordDataFormat("Formatted"), + onspring.WithPaging(onspring.ForPageNumber(1), onspring.WithPageSize(1)), + ) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber != 1 { + t.Errorf("Expected page number 1, got %d", page.PageNumber) + } + + if page.PageSize != 1 { + t.Errorf("Expected page size 1, got %d", page.PageSize) + } + + if len(page.Items) != 1 { + t.Errorf("Expected 1 item, got %d", len(page.Items)) + } + }) + + t.Run("should return a 401 error when an invalid API key is passed", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + _, err := invalidClient.Records.List(ctx, surveyId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when the api key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + + _, err := client.Records.List(ctx, appIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) + + t.Run("ListAll", func(t *testing.T) { + t.Run("should iterate all records for an app", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + var records []onspring.Record + + for record, err := range client.Records.ListAll(ctx, surveyId, onspring.WithPaging(onspring.WithPageSize(1))) { + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + records = append(records, record) + } + + if len(records) == 0 { + t.Error("Expected to iterate at least one record") + } + }) + }) + + t.Run("GetMany", func(t *testing.T) { + t.Run("should get records", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + batch, err := client.Records.GetMany(ctx, onspring.GetManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{recordId}, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if batch.Count == 0 { + t.Error("Expected count to not be zero") + } + + if len(batch.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, record := range batch.Items { + if record.AppId != surveyId { + t.Errorf("Expected appId %d, got %d", surveyId, record.AppId) + } + + if record.RecordId == 0 { + t.Error("Expected recordId to not be zero") + } + } + }) + + t.Run("should get records when field ids and data format are passed as parameters", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + batch, err := client.Records.GetMany(ctx, onspring.GetManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{recordId}, + FieldIds: []int{textFieldId}, + DataFormat: "Formatted", + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if batch.Count == 0 { + t.Error("Expected count to not be zero") + } + + if len(batch.Items) == 0 { + t.Error("Expected items to not be empty") + } + }) + + t.Run("should return a 400 error if too many record ids are passed", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + ids := make([]int, 101) + for i := range ids { + ids[i] = i + 1 + } + + _, err := client.Records.GetMany(ctx, onspring.GetManyRecordsRequest{ + AppId: surveyId, + RecordIds: ids, + }) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error if the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + _, err := invalidClient.Records.GetMany(ctx, onspring.GetManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{recordId}, + }) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error if the user does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + _, err := client.Records.GetMany(ctx, onspring.GetManyRecordsRequest{ + AppId: appIdNoAccess, + RecordIds: []int{recordId}, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) + + t.Run("Query", func(t *testing.T) { + t.Run("should return records", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + page, err := client.Records.Query(ctx, onspring.QueryRecordsRequest{ + AppId: surveyId, + Filter: filter, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber == 0 { + t.Error("Expected page number to not be zero") + } + + if page.TotalRecords == 0 { + t.Error("Expected total records to not be zero") + } + + if len(page.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, record := range page.Items { + if record.AppId != surveyId { + t.Errorf("Expected appId %d, got %d", surveyId, record.AppId) + } + + if record.RecordId == 0 { + t.Error("Expected recordId to not be zero") + } + } + }) + + t.Run("should return records when data format, paging information, and fields are passed as parameters", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + page, err := client.Records.Query( + ctx, + onspring.QueryRecordsRequest{ + AppId: surveyId, + Filter: filter, + FieldIds: []int{textFieldId}, + DataFormat: "Formatted", + }, + onspring.ForPageNumber(1), + onspring.WithPageSize(1), + ) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber != 1 { + t.Errorf("Expected page number 1, got %d", page.PageNumber) + } + + if page.PageSize != 1 { + t.Errorf("Expected page size 1, got %d", page.PageSize) + } + + if len(page.Items) != 1 { + t.Errorf("Expected 1 item, got %d", len(page.Items)) + } + }) + + t.Run("should return a 400 error if page size is invalid", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + _, err := client.Records.Query( + ctx, + onspring.QueryRecordsRequest{ + AppId: surveyId, + Filter: filter, + }, + onspring.WithPageSize(1001), + ) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error if the API key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + _, err := invalidClient.Records.Query(ctx, onspring.QueryRecordsRequest{ + AppId: surveyId, + Filter: filter, + }) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error if the API key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + _, err := client.Records.Query(ctx, onspring.QueryRecordsRequest{ + AppId: appIdNoAccess, + Filter: filter, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) + + t.Run("QueryAll", func(t *testing.T) { + t.Run("should iterate all records matching a query", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + autoNumberField := requireEnvInt(t, "TEST_SURVEY_AUTO_NUMBER_FIELD") + filter := fmt.Sprintf("%d gt 0", autoNumberField) + + var records []onspring.Record + + for record, err := range client.Records.QueryAll( + ctx, + onspring.QueryRecordsRequest{ + AppId: surveyId, + Filter: filter, + }, + onspring.WithPageSize(1), + ) { + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + records = append(records, record) + } + + if len(records) == 0 { + t.Error("Expected to iterate at least one record") + } + }) + }) + + t.Run("Save", func(t *testing.T) { + t.Run("should add a record when no record id is passed", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + response, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: surveyId, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "integration test record", + }, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if response.Id == 0 { + t.Error("Expected record id to not be zero") + } + + if response.Warnings == nil { + t.Error("Expected warnings to not be nil") + } + + t.Cleanup(func() { + _ = client.Records.Delete(context.Background(), surveyId, response.Id) + }) + }) + + t.Run("should update a record when a record id is passed", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + recordId := addTestRecord(t, client, surveyId, textFieldId) + + response, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: surveyId, + RecordId: &recordId, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "updated integration test record", + }, + }) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if response.Id == 0 { + t.Error("Expected record id to not be zero") + } + }) + + t.Run("should return a 400 error when field data is empty", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + _, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: surveyId, + Fields: map[string]any{}, + }) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error when the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + _, err := invalidClient.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: surveyId, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "test", + }, + }) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when the api key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + + _, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: appIdNoAccess, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "test", + }, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when the record id is not found", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + recordId := 0 + + _, err := client.Records.Save(ctx, onspring.SaveRecordRequest{ + AppId: surveyId, + RecordId: &recordId, + Fields: map[string]any{ + fmt.Sprintf("%d", textFieldId): "test", + }, + }) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("should delete a record", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + recordId := addTestRecord(t, client, surveyId, textFieldId) + + err := client.Records.Delete(ctx, surveyId, recordId) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("should return a 401 error when the API key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + err := invalidClient.Records.Delete(ctx, surveyId, recordId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when the API key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + recordId := requireEnvInt(t, "TEST_SURVEY_RECORD_ID") + + err := client.Records.Delete(ctx, appIdNoAccess, recordId) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error when the record does not exist", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + err := client.Records.Delete(ctx, surveyId, 0) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("DeleteMany", func(t *testing.T) { + t.Run("should delete records", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + textFieldId := requireEnvInt(t, "TEST_TEXT_FIELD") + recordId1 := addTestRecord(t, client, surveyId, textFieldId) + recordId2 := addTestRecord(t, client, surveyId, textFieldId) + + err := client.Records.DeleteMany(ctx, onspring.DeleteManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{recordId1, recordId2}, + }) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("should return a 400 error when no record ids are provided", func(t *testing.T) { + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + err := client.Records.DeleteMany(ctx, onspring.DeleteManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{}, + }) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error when an invalid API key is used", func(t *testing.T) { + invalidClient := createInvalidClient(t) + surveyId := requireEnvInt(t, "TEST_SURVEY_ID") + + err := invalidClient.Records.DeleteMany(ctx, onspring.DeleteManyRecordsRequest{ + AppId: surveyId, + RecordIds: []int{1}, + }) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error when the API key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + + err := client.Records.DeleteMany(ctx, onspring.DeleteManyRecordsRequest{ + AppId: appIdNoAccess, + RecordIds: []int{1}, + }) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) +} diff --git a/reports_integration_test.go b/reports_integration_test.go new file mode 100644 index 0000000..62ad94e --- /dev/null +++ b/reports_integration_test.go @@ -0,0 +1,203 @@ +//go:build integration + +package onspring_test + +import ( + "context" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestReportsIntegration(t *testing.T) { + loadEnvFile(t) + client := createClient(t) + ctx := context.Background() + + t.Run("Get", func(t *testing.T) { + t.Run("should return a report", func(t *testing.T) { + reportId := requireEnvInt(t, "TEST_REPORT") + + report, err := client.Reports.Get(ctx, reportId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if report.Columns == nil { + t.Error("Expected columns to not be nil") + } + + if report.Rows == nil { + t.Error("Expected rows to not be nil") + } + + for _, row := range report.Rows { + if row.Cells == nil { + t.Error("Expected cells to not be nil") + } + } + }) + + t.Run("should return report data for a report with chart data when report data is requested", func(t *testing.T) { + reportId := requireEnvInt(t, "TEST_REPORT_WITH_CHART_DATA") + + report, err := client.Reports.Get(ctx, reportId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if report.Columns == nil { + t.Error("Expected columns to not be nil") + } + + if report.Rows == nil { + t.Error("Expected rows to not be nil") + } + }) + + t.Run("should return chart data for a report with a chart when chart data is requested", func(t *testing.T) { + reportId := requireEnvInt(t, "TEST_REPORT_WITH_CHART_DATA") + + report, err := client.Reports.Get( + ctx, + reportId, + onspring.WithDataFormat("Raw"), + onspring.WithDataType("ChartData"), + ) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if report.Columns == nil { + t.Error("Expected columns to not be nil") + } + + if report.Rows == nil { + t.Error("Expected rows to not be nil") + } + }) + + t.Run("should return a 400 error if chart data is requested for a report without chart data", func(t *testing.T) { + reportId := requireEnvInt(t, "TEST_REPORT") + + _, err := client.Reports.Get( + ctx, + reportId, + onspring.WithDataFormat("Raw"), + onspring.WithDataType("ChartData"), + ) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error if the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + reportId := requireEnvInt(t, "TEST_REPORT") + + _, err := invalidClient.Reports.Get(ctx, reportId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error if the api key does not have access to the report", func(t *testing.T) { + reportIdNoAccess := requireEnvInt(t, "TEST_REPORT_NO_ACCESS") + + _, err := client.Reports.Get(ctx, reportIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + + t.Run("should return a 404 error if the report does not exist", func(t *testing.T) { + _, err := client.Reports.Get(ctx, 0) + + assertAPIError(t, err, http.StatusNotFound) + }) + }) + + t.Run("List", func(t *testing.T) { + t.Run("should return a list of reports", func(t *testing.T) { + appId := requireEnvInt(t, "TEST_APP_ID") + + page, err := client.Reports.List(ctx, appId) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if page.PageNumber == 0 { + t.Error("Expected page number to not be zero") + } + + if page.TotalRecords == 0 { + t.Error("Expected total records to not be zero") + } + + if len(page.Items) == 0 { + t.Error("Expected items to not be empty") + } + + for _, report := range page.Items { + if report.Id == 0 { + t.Error("Expected report id to not be zero") + } + + if report.AppId == 0 { + t.Error("Expected report appId to not be zero") + } + + if report.Name == "" { + t.Error("Expected report name to not be empty") + } + } + }) + + t.Run("should return a 400 error if page size is invalid", func(t *testing.T) { + appId := requireEnvInt(t, "TEST_APP_ID") + + _, err := client.Reports.List(ctx, appId, onspring.WithPageSize(1001)) + + assertAPIError(t, err, http.StatusBadRequest) + }) + + t.Run("should return a 401 error if the api key is invalid", func(t *testing.T) { + invalidClient := createInvalidClient(t) + appId := requireEnvInt(t, "TEST_APP_ID") + + _, err := invalidClient.Reports.List(ctx, appId) + + assertAPIError(t, err, http.StatusUnauthorized) + }) + + t.Run("should return a 403 error if the api key does not have access to the app", func(t *testing.T) { + appIdNoAccess := requireEnvInt(t, "TEST_APP_ID_NO_ACCESS") + + _, err := client.Reports.List(ctx, appIdNoAccess) + + assertAPIError(t, err, http.StatusForbidden) + }) + }) + + t.Run("ListAll", func(t *testing.T) { + t.Run("should iterate all reports for an app", func(t *testing.T) { + appId := requireEnvInt(t, "TEST_APP_ID") + + var reports []onspring.Report + + for report, err := range client.Reports.ListAll(ctx, appId, onspring.WithPageSize(1)) { + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + reports = append(reports, report) + } + + if len(reports) == 0 { + t.Error("Expected to iterate at least one report") + } + }) + }) +} diff --git a/testdata/test-attachment.txt b/testdata/test-attachment.txt new file mode 100644 index 0000000..3eae1d0 --- /dev/null +++ b/testdata/test-attachment.txt @@ -0,0 +1 @@ +This is a test attachment. \ No newline at end of file diff --git a/testdata/test-image.jpeg b/testdata/test-image.jpeg new file mode 100644 index 0000000..b493033 Binary files /dev/null and b/testdata/test-image.jpeg differ