diff --git a/README.md b/README.md index 43b2a5d..a09d96f 100644 --- a/README.md +++ b/README.md @@ -846,7 +846,7 @@ Anyone is welcome to contribute. - Open an issue or discussion post to track the effort - Fork this repository, then clone it -- Replace dependency by pointing to your locally cloned `go-salesforce` in your module's `go.mod` +- Place this in your own module's `go.mod` to enable testing local changes - `replace github.com/k-capehart/go-salesforce => /path_to_local_fork/` - Run tests - `go test -cover` diff --git a/bulk.go b/bulk.go index f2f90bf..8d3e18a 100644 --- a/bulk.go +++ b/bulk.go @@ -9,10 +9,10 @@ import ( "fmt" "io" "net/http" - "os" "strconv" "time" + "github.com/spf13/afero" "k8s.io/apimachinery/pkg/util/wait" ) @@ -59,6 +59,8 @@ const ( queryJobType = "query" ) +var appFs = afero.NewOsFs() // afero.Fs type is a wrapper around os functions, allowing us to mock it in tests + func updateJobState(job bulkJob, state string, auth authentication) error { job.State = state body, _ := json.Marshal(job) @@ -311,7 +313,7 @@ func mapsToCSVSlices(maps []map[string]string) ([][]string, error) { } func readCSVFile(filePath string) ([][]string, error) { - file, fileErr := os.Open(filePath) + file, fileErr := appFs.Open(filePath) if fileErr != nil { return nil, fileErr } @@ -327,7 +329,7 @@ func readCSVFile(filePath string) ([][]string, error) { } func writeCSVFile(filePath string, data [][]string) error { - file, fileErr := os.Create(filePath) + file, fileErr := appFs.Create(filePath) if fileErr != nil { return fileErr } @@ -408,7 +410,7 @@ func doBulkJob(auth authentication, sObjectName string, fieldName string, operat if waitForResults { c := make(chan error, len(jobIds)) for _, id := range jobIds { - go waitForJobResultsAsync(auth, id, ingestJobType, time.Second, c) + go waitForJobResultsAsync(auth, id, ingestJobType, (time.Second / 2), c) } jobErrors = <-c } @@ -467,7 +469,7 @@ func doBulkJobWithFile(auth authentication, sObjectName string, fieldName string if waitForResults { c := make(chan error, len(jobIds)) for _, id := range jobIds { - go waitForJobResultsAsync(auth, id, ingestJobType, time.Second, c) + go waitForJobResultsAsync(auth, id, ingestJobType, (time.Second / 2), c) } jobErrors = <-c } @@ -494,7 +496,7 @@ func doQueryBulk(auth authentication, filePath string, query string) error { return newErr } - pollErr := waitForJobResults(auth, job.Id, queryJobType, time.Second) + pollErr := waitForJobResults(auth, job.Id, queryJobType, (time.Second / 2)) if pollErr != nil { return pollErr } diff --git a/bulk_test.go b/bulk_test.go index 46ac857..4f12a06 100644 --- a/bulk_test.go +++ b/bulk_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" "time" + + "github.com/spf13/afero" ) func Test_createBulkJob(t *testing.T) { @@ -706,3 +708,157 @@ func Test_collectQueryResults(t *testing.T) { }) } } + +func Test_uploadJobData(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI[len(r.RequestURI)-8:] == "/batches" { + w.WriteHeader(http.StatusCreated) + } else { + w.WriteHeader(http.StatusOK) + } + })) + sfAuth := authentication{ + InstanceUrl: server.URL, + AccessToken: "accesstokenvalue", + } + defer server.Close() + + badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI[len(r.RequestURI)-8:] == "/batches" { + w.WriteHeader(http.StatusBadRequest) + } else { + w.WriteHeader(http.StatusOK) + } + })) + badAuth := authentication{ + InstanceUrl: badServer.URL, + AccessToken: "accesstokenvalue", + } + defer badServer.Close() + + type args struct { + auth authentication + data string + bulkJob bulkJob + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "update job state success", + args: args{ + auth: sfAuth, + data: "data", + bulkJob: bulkJob{}, + }, + wantErr: false, + }, + { + name: "update job state fail", + args: args{ + auth: badAuth, + data: "data", + bulkJob: bulkJob{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := uploadJobData(tt.args.auth, tt.args.data, tt.args.bulkJob); (err != nil) != tt.wantErr { + t.Errorf("uploadJobData() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_readCSVFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + if err := appFs.MkdirAll("data", 0755); err != nil { + t.Fatalf("error creating directory in virtual file system") + } + if err := afero.WriteFile(appFs, "data/data.csv", []byte("123"), 0644); err != nil { + t.Fatalf("error creating file in virtual file system") + } + + type args struct { + filePath string + } + tests := []struct { + name string + args args + want [][]string + wantErr bool + }{ + { + name: "read file successfully", + args: args{ + filePath: "data/data.csv", + }, + want: [][]string{{"123"}}, + wantErr: false, + }, + { + name: "read file failure", + args: args{ + filePath: "data/does_not_exist.csv", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := readCSVFile(tt.args.filePath) + if (err != nil) != tt.wantErr { + t.Errorf("readCSVFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("readCSVFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_writeCSVFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + + type args struct { + filePath string + data [][]string + } + tests := []struct { + name string + args args + want [][]string + wantErr bool + }{ + { + name: "write file successfully", + args: args{ + filePath: "data/export.csv", + data: [][]string{{"123"}}, + }, + want: [][]string{{"123"}}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := writeCSVFile(tt.args.filePath, tt.args.data); (err != nil) != tt.wantErr { + t.Errorf("writeCSVFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + got, err := readCSVFile("data/export.csv") + if err != nil { + t.Errorf(err.Error()) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("writeCSVFile() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index 27dd774..e994db9 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,14 @@ require github.com/mitchellh/mapstructure v1.5.0 require github.com/forcedotcom/go-soql v0.0.0-20220705175410-00f698360bee -require k8s.io/apimachinery v0.29.4 +require ( + github.com/spf13/afero v1.11.0 + k8s.io/apimachinery v0.29.4 +) require ( github.com/go-logr/logr v1.3.0 // indirect + golang.org/x/text v0.14.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect ) diff --git a/go.sum b/go.sum index 978ca6d..869a321 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/salesforce.go b/salesforce.go index 273ad3b..4a04ddb 100644 --- a/salesforce.go +++ b/salesforce.go @@ -131,14 +131,16 @@ func validateCollections(sf Salesforce, records any, batchSize int) error { return nil } -func validateBulk(sf Salesforce, records any, batchSize int) error { +func validateBulk(sf Salesforce, records any, batchSize int, isFile bool) error { authErr := validateAuth(sf) if authErr != nil { return authErr } - typErr := validateOfTypeSlice(records) - if typErr != nil { - return typErr + if !isFile { + typErr := validateOfTypeSlice(records) + if typErr != nil { + return typErr + } } batchSizeErr := validateBatchSizeWithinRange(batchSize, bulkBatchSizeMax) if batchSizeErr != nil { @@ -464,7 +466,7 @@ func (sf *Salesforce) QueryStructBulkExport(soqlStruct any, filePath string) err } func (sf *Salesforce) InsertBulk(sObjectName string, records any, batchSize int, waitForResults bool) ([]string, error) { - validationErr := validateBulk(*sf, records, batchSize) + validationErr := validateBulk(*sf, records, batchSize, false) if validationErr != nil { return []string{}, validationErr } @@ -478,9 +480,9 @@ func (sf *Salesforce) InsertBulk(sObjectName string, records any, batchSize int, } func (sf *Salesforce) InsertBulkFile(sObjectName string, filePath string, batchSize int, waitForResults bool) ([]string, error) { - authErr := validateAuth(*sf) - if authErr != nil { - return []string{}, authErr + validationErr := validateBulk(*sf, nil, batchSize, true) + if validationErr != nil { + return []string{}, validationErr } jobIds, bulkErr := doBulkJobWithFile(*sf.auth, sObjectName, "", insertOperation, filePath, batchSize, waitForResults) @@ -492,7 +494,7 @@ func (sf *Salesforce) InsertBulkFile(sObjectName string, filePath string, batchS } func (sf *Salesforce) UpdateBulk(sObjectName string, records any, batchSize int, waitForResults bool) ([]string, error) { - validationErr := validateBulk(*sf, records, batchSize) + validationErr := validateBulk(*sf, records, batchSize, false) if validationErr != nil { return []string{}, validationErr } @@ -506,9 +508,9 @@ func (sf *Salesforce) UpdateBulk(sObjectName string, records any, batchSize int, } func (sf *Salesforce) UpdateBulkFile(sObjectName string, filePath string, batchSize int, waitForResults bool) ([]string, error) { - authErr := validateAuth(*sf) - if authErr != nil { - return []string{}, authErr + validationErr := validateBulk(*sf, nil, batchSize, true) + if validationErr != nil { + return []string{}, validationErr } jobIds, bulkErr := doBulkJobWithFile(*sf.auth, sObjectName, "", updateOperation, filePath, batchSize, waitForResults) @@ -520,7 +522,7 @@ func (sf *Salesforce) UpdateBulkFile(sObjectName string, filePath string, batchS } func (sf *Salesforce) UpsertBulk(sObjectName string, externalIdFieldName string, records any, batchSize int, waitForResults bool) ([]string, error) { - validationErr := validateBulk(*sf, records, batchSize) + validationErr := validateBulk(*sf, records, batchSize, false) if validationErr != nil { return []string{}, validationErr } @@ -534,9 +536,9 @@ func (sf *Salesforce) UpsertBulk(sObjectName string, externalIdFieldName string, } func (sf *Salesforce) UpsertBulkFile(sObjectName string, externalIdFieldName string, filePath string, batchSize int, waitForResults bool) ([]string, error) { - authErr := validateAuth(*sf) - if authErr != nil { - return []string{}, authErr + validationErr := validateBulk(*sf, nil, batchSize, true) + if validationErr != nil { + return []string{}, validationErr } jobIds, bulkErr := doBulkJobWithFile(*sf.auth, sObjectName, externalIdFieldName, upsertOperation, filePath, batchSize, waitForResults) @@ -548,7 +550,7 @@ func (sf *Salesforce) UpsertBulkFile(sObjectName string, externalIdFieldName str } func (sf *Salesforce) DeleteBulk(sObjectName string, records any, batchSize int, waitForResults bool) ([]string, error) { - validationErr := validateBulk(*sf, records, batchSize) + validationErr := validateBulk(*sf, records, batchSize, false) if validationErr != nil { return []string{}, validationErr } @@ -562,9 +564,9 @@ func (sf *Salesforce) DeleteBulk(sObjectName string, records any, batchSize int, } func (sf *Salesforce) DeleteBulkFile(sObjectName string, filePath string, batchSize int, waitForResults bool) ([]string, error) { - authErr := validateAuth(*sf) - if authErr != nil { - return []string{}, authErr + validationErr := validateBulk(*sf, nil, batchSize, true) + if validationErr != nil { + return []string{}, validationErr } jobIds, bulkErr := doBulkJobWithFile(*sf.auth, sObjectName, "", deleteOperation, filePath, batchSize, waitForResults) diff --git a/salesforce_test.go b/salesforce_test.go index 81235b0..8f9d4e5 100644 --- a/salesforce_test.go +++ b/salesforce_test.go @@ -10,6 +10,8 @@ import ( "reflect" "strings" "testing" + + "github.com/spf13/afero" ) func setupTestServer(body any, status int) (*httptest.Server, authentication) { @@ -513,6 +515,7 @@ func Test_validateBulk(t *testing.T) { sf Salesforce records any batchSize int + isFile bool } tests := []struct { name string @@ -527,6 +530,7 @@ func Test_validateBulk(t *testing.T) { }}, records: []account{}, batchSize: 10000, + isFile: false, }, wantErr: false, }, @@ -536,6 +540,7 @@ func Test_validateBulk(t *testing.T) { sf: Salesforce{}, records: []account{}, batchSize: 10000, + isFile: false, }, wantErr: true, }, @@ -547,6 +552,7 @@ func Test_validateBulk(t *testing.T) { }}, records: 0, batchSize: 10000, + isFile: false, }, wantErr: true, }, @@ -558,13 +564,26 @@ func Test_validateBulk(t *testing.T) { }}, records: []account{}, batchSize: 0, + isFile: false, }, wantErr: true, }, + { + name: "validation_success_file", + args: args{ + sf: Salesforce{auth: &authentication{ + AccessToken: "1234", + }}, + records: nil, + batchSize: 2000, + isFile: true, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := validateBulk(tt.args.sf, tt.args.records, tt.args.batchSize); (err != nil) != tt.wantErr { + if err := validateBulk(tt.args.sf, tt.args.records, tt.args.batchSize, tt.args.isFile); (err != nil) != tt.wantErr { t.Errorf("validateBulk() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -2101,3 +2120,500 @@ func TestSalesforce_GetJobResults(t *testing.T) { }) } } + +func TestSalesforce_InsertBulkFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + if err := appFs.MkdirAll("data", 0755); err != nil { + t.Fatalf("error creating directory in virtual file system") + } + if err := afero.WriteFile(appFs, "data/data.csv", []byte("header\nrow"), 0644); err != nil { + t.Fatalf("error creating file in virtual file system") + } + + job := bulkJob{ + Id: "1234", + State: jobStateOpen, + } + server, sfAuth := setupTestServer(job, http.StatusOK) + defer server.Close() + + type fields struct { + auth *authentication + } + type args struct { + sObjectName string + filePath string + batchSize int + waitForResults bool + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + { + name: "insert bulk data successfully", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 2000, + waitForResults: false, + }, + want: []string{"1234"}, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 10001, + waitForResults: false, + }, + want: []string{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + got, err := sf.InsertBulkFile(tt.args.sObjectName, tt.args.filePath, tt.args.batchSize, tt.args.waitForResults) + if (err != nil) != tt.wantErr { + t.Errorf("Salesforce.InsertBulkFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Salesforce.InsertBulkFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSalesforce_UpdateBulkFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + if err := appFs.MkdirAll("data", 0755); err != nil { + t.Fatalf("error creating directory in virtual file system") + } + if err := afero.WriteFile(appFs, "data/data.csv", []byte("header\nrow"), 0644); err != nil { + t.Fatalf("error creating file in virtual file system") + } + + job := bulkJob{ + Id: "1234", + State: jobStateOpen, + } + server, sfAuth := setupTestServer(job, http.StatusOK) + defer server.Close() + + type fields struct { + auth *authentication + } + type args struct { + sObjectName string + filePath string + batchSize int + waitForResults bool + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + { + name: "update bulk data successfully", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 2000, + waitForResults: false, + }, + want: []string{"1234"}, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 10001, + waitForResults: false, + }, + want: []string{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + got, err := sf.UpdateBulkFile(tt.args.sObjectName, tt.args.filePath, tt.args.batchSize, tt.args.waitForResults) + if (err != nil) != tt.wantErr { + t.Errorf("Salesforce.UpdateBulkFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Salesforce.UpdateBulkFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSalesforce_UpsertBulkFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + if err := appFs.MkdirAll("data", 0755); err != nil { + t.Fatalf("error creating directory in virtual file system") + } + if err := afero.WriteFile(appFs, "data/data.csv", []byte("header\nrow"), 0644); err != nil { + t.Fatalf("error creating file in virtual file system") + } + + job := bulkJob{ + Id: "1234", + State: jobStateOpen, + } + server, sfAuth := setupTestServer(job, http.StatusOK) + defer server.Close() + + type fields struct { + auth *authentication + } + type args struct { + sObjectName string + externalIdFieldName string + filePath string + batchSize int + waitForResults bool + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + { + name: "upsert bulk data successfully", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + externalIdFieldName: "ExternalId__c", + filePath: "data/data.csv", + batchSize: 2000, + waitForResults: false, + }, + want: []string{"1234"}, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + externalIdFieldName: "ExternalId__c", + filePath: "data/data.csv", + batchSize: 10001, + waitForResults: false, + }, + want: []string{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + got, err := sf.UpsertBulkFile(tt.args.sObjectName, tt.args.externalIdFieldName, tt.args.filePath, tt.args.batchSize, tt.args.waitForResults) + if (err != nil) != tt.wantErr { + t.Errorf("Salesforce.UpsertBulkFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Salesforce.UpsertBulkFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSalesforce_DeleteBulkFile(t *testing.T) { + appFs = afero.NewMemMapFs() // replace appFs with mocked file system + if err := appFs.MkdirAll("data", 0755); err != nil { + t.Fatalf("error creating directory in virtual file system") + } + if err := afero.WriteFile(appFs, "data/data.csv", []byte("header\nrow"), 0644); err != nil { + t.Fatalf("error creating file in virtual file system") + } + + job := bulkJob{ + Id: "1234", + State: jobStateOpen, + } + server, sfAuth := setupTestServer(job, http.StatusOK) + defer server.Close() + + type fields struct { + auth *authentication + } + type args struct { + sObjectName string + filePath string + batchSize int + waitForResults bool + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + { + name: "delete bulk data successfully", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 2000, + waitForResults: false, + }, + want: []string{"1234"}, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + auth: &sfAuth, + }, + args: args{ + sObjectName: "Account", + filePath: "data/data.csv", + batchSize: 10001, + waitForResults: false, + }, + want: []string{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + got, err := sf.DeleteBulkFile(tt.args.sObjectName, tt.args.filePath, tt.args.batchSize, tt.args.waitForResults) + if (err != nil) != tt.wantErr { + t.Errorf("Salesforce.DeleteBulkFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Salesforce.DeleteBulkFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSalesforce_QueryBulkExport(t *testing.T) { + job := bulkJob{ + Id: "1234", + State: jobStateJobComplete, + } + jobResults := BulkJobResults{ + Id: "1234", + State: jobStateJobComplete, + NumberRecordsFailed: 0, + ErrorMessage: "", + } + jobCreationRespBody, _ := json.Marshal(job) + jobResultsRespBody, _ := json.Marshal(jobResults) + csvData := `"col"` + "\n" + `"row"` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI[len(r.RequestURI)-6:] == "/query" { + w.WriteHeader(http.StatusOK) + if _, err := w.Write(jobCreationRespBody); err != nil { + t.Fatalf(err.Error()) + } + } else if r.RequestURI[len(r.RequestURI)-5:] == "/1234" { + if _, err := w.Write(jobResultsRespBody); err != nil { + t.Fatalf(err.Error()) + } + } else if r.RequestURI[len(r.RequestURI)-8:] == "/results" { + w.Header().Add("Sforce-Locator", "") + w.Header().Add("Sforce-Numberofrecords", "1") + if _, err := w.Write([]byte(csvData)); err != nil { + t.Fatalf(err.Error()) + } + } + })) + sfAuth := authentication{ + InstanceUrl: server.URL, + AccessToken: "accesstokenvalue", + } + defer server.Close() + + badServer, badAuth := setupTestServer(job, http.StatusBadRequest) + defer badServer.Close() + + type fields struct { + auth *authentication + } + type args struct { + query string + filePath string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "export data successfully", + fields: fields{ + &sfAuth, + }, + args: args{ + query: "SELECT Id FROM Account", + filePath: "data/export.csv", + }, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + &badAuth, + }, + args: args{ + query: "SELECT Id FROM Account", + filePath: "data/export.csv", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + if err := sf.QueryBulkExport(tt.args.query, tt.args.filePath); (err != nil) != tt.wantErr { + t.Errorf("Salesforce.QueryBulkExport() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSalesforce_QueryStructBulkExport(t *testing.T) { + type account struct { + Id string + Name string + } + job := bulkJob{ + Id: "1234", + State: jobStateJobComplete, + } + jobResults := BulkJobResults{ + Id: "1234", + State: jobStateJobComplete, + NumberRecordsFailed: 0, + ErrorMessage: "", + } + jobCreationRespBody, _ := json.Marshal(job) + jobResultsRespBody, _ := json.Marshal(jobResults) + csvData := `"col"` + "\n" + `"row"` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI[len(r.RequestURI)-6:] == "/query" { + w.WriteHeader(http.StatusOK) + if _, err := w.Write(jobCreationRespBody); err != nil { + t.Fatalf(err.Error()) + } + } else if r.RequestURI[len(r.RequestURI)-5:] == "/1234" { + if _, err := w.Write(jobResultsRespBody); err != nil { + t.Fatalf(err.Error()) + } + } else if r.RequestURI[len(r.RequestURI)-8:] == "/results" { + w.Header().Add("Sforce-Locator", "") + w.Header().Add("Sforce-Numberofrecords", "1") + if _, err := w.Write([]byte(csvData)); err != nil { + t.Fatalf(err.Error()) + } + } + })) + sfAuth := authentication{ + InstanceUrl: server.URL, + AccessToken: "accesstokenvalue", + } + defer server.Close() + + badServer, badAuth := setupTestServer(job, http.StatusBadRequest) + defer badServer.Close() + + type fields struct { + auth *authentication + } + type args struct { + soqlStruct any + filePath string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "export data successfully", + fields: fields{ + &sfAuth, + }, + args: args{ + soqlStruct: account{}, + filePath: "data/export.csv", + }, + wantErr: false, + }, + { + name: "validation error", + fields: fields{ + &badAuth, + }, + args: args{ + soqlStruct: account{}, + filePath: "data/export.csv", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sf := &Salesforce{ + auth: tt.fields.auth, + } + if err := sf.QueryStructBulkExport(tt.args.soqlStruct, tt.args.filePath); (err != nil) != tt.wantErr { + t.Errorf("Salesforce.QueryStructBulkExport() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}