Repository: amp-labs/connectors
Pull request #1000 (2024-08-27 21:16:15)

feat: Sync Read capability with OpenAPI
# Changes

* Every connector that serves Metadata from `schema.json` has additional check during **Read** that prunes unsupported objects (not part of metadata)
* OpenAPI did some special mapping between `ObjectName` and `JsonPath` holding that object. This logically belongs to the connector and is moved as `DefaultMap` called `ObjectNameToResponseField`. 

# Benefits
`ObjectNameToResponseField` is used by `ZendeskSupport` and `Intercom`.

Because of the move it is not only beneficial for OpenAPI scripts to extract schemas but now it offers correct JSON resolution!
**Zendesk Support Read:**
![image](https://github.com/user-attachments/assets/5653e7e0-407a-4469-b984-3bd73c44c958)

# Summary
This removes the gap between ListObjectMetadata and Read methods. It syncs both, making Read cover **no more** and **no less** than what ListObjetMetadata returns.

Cobalt0s: > [!WARNING]
> <b>This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1000?utm_source=stack-comment-downstack-mergeability-warning" >on Graphite</a>.</b>
> <a href="https://graphite.dev/docs/merge-pull-requests">Learn more</a>

* **#1001** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1001?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1000** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1000?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈
* **#999** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/999?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`

This stack of pull requests is managed by Graphite. <a href="https://stacking.dev/?utm_source=stack-comment">Learn more about stacking.</a>
<h2></h2>

 Join @Cobalt0s and the rest of your teammates on <a href="https://graphite.dev?utm-source=stack-comment"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="11px" height="11px"/> <b>Graphite</b></a>
  <!-- Current dependencies on/for this PR: -->
RajatPawar: > # Changes
> The following only apply to **connectors** that use static `schema.json` when serving ListObjectMetadata:
> 
> 1. ObjectName must be passed to Read method.
> 2. Read will error out `common.ErrOperationNotSupportedForObject` for ObjectNames that are not found under schemas.json.
> 3. Parsing `schema.json` is done on package import. JSON parsing error is ignored, we assume the file is valid. If at least one test exists this will be caught.
> 4. Added mapping ObjectName to Response Field for **ZendeskSupport** and **Intercom**.
> 5. Fix ZendeskSupport to locate correct JSON path even for special objects by relying on `ObjectNameToResponseField` map described in the step 4. Note: Intercom returns special `type` field that tells where the array is located so it already covers all ObjectName space.
> 6. Log error when any OpenAPI schema cannot be processed. If we update OpenAPI file we will be notified about unexpected changes.
> 
> # PS
> Let me know if this PR needs splitting.

Please split this up and request a review 
Cobalt0s: > > # Changes
> > The following only apply to **connectors** that use static `schema.json` when serving ListObjectMetadata:
> > 
> > 1. ObjectName must be passed to Read method.
> > 2. Read will error out `common.ErrOperationNotSupportedForObject` for ObjectNames that are not found under schemas.json.
> > 3. Parsing `schema.json` is done on package import. JSON parsing error is ignored, we assume the file is valid. If at least one test exists this will be caught.
> > 4. Added mapping ObjectName to Response Field for **ZendeskSupport** and **Intercom**.
> > 5. Fix ZendeskSupport to locate correct JSON path even for special objects by relying on `ObjectNameToResponseField` map described in the step 4. Note: Intercom returns special `type` field that tells where the array is located so it already covers all ObjectName space.
> > 6. Log error when any OpenAPI schema cannot be processed. If we update OpenAPI file we will be notified about unexpected changes.
> > 
> > # PS
> > Let me know if this PR needs splitting.
> 
> Please split this up and request a review

Ok, lets start with parameter validation. I started with enforcing ObjectName to be required for Read method.
Generalized it more. https://github.com/amp-labs/connectors/pull/1013 The PR is bigger but it addresses 1 issue at the large scale. Lauren asked a while ago about ObjectName being required. 

I will be splitting this PR bit by bit by concept (this helped me generalize the problem).
Cobalt0s: @RajatPawar I repurpopsed this PR and it was split into chunks. Check graphite for the full tree structure. It is ready for review.

Patch:
diff --git a/providers/intercom/objectNames.go b/providers/intercom/objectNames.go
new file mode 100644
index 00000000..c016caee
--- /dev/null
+++ b/providers/intercom/objectNames.go
@@ -0,0 +1,26 @@
+package intercom
+
+import (
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/providers/intercom/metadata"
+)
+
+// Supported object names can be found under schemas.json.
+var supportedObjectsByRead = handy.NewSet( //nolint:gochecknoglobals
+	metadata.Schemas.GetObjectNames(),
+)
+
+// ObjectNameToResponseField maps ObjectName to the response field name which contains that object.
+var ObjectNameToResponseField = handy.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+	"admins":        "admins",
+	"teams":         "teams",
+	"ticket_types":  "ticket_types",
+	"events":        "events",
+	"segments":      "segments",
+	"activity_logs": "activity_logs",
+},
+	func(key string) string {
+		// Other objects are mapped to `data`.
+		return "data"
+	},
+)
diff --git a/providers/intercom/parse.go b/providers/intercom/parse.go
index eb0ba004..edeafa06 100644
--- a/providers/intercom/parse.go
+++ b/providers/intercom/parse.go
@@ -80,8 +80,17 @@ func getNextPageStringURL(node *ajson.Node) (string, error) {
 	return jsonquery.New(node, "pages").StrWithDefault("next", "")
 }
 
-// The key that stores array in response payload will be dynamically figured out.
-// Ex: {"data": []} vs {"teams":[]} vs {"segments":[]}.
+// Intercom returns a type field which tells us where the response array is located,
+// which is why we do not need to hardcode the mapping. If we need to override this at any point,
+// we can add the mapping here.
+//
+// Other connectors don't have the ability to infer field names programmatically, so they rely on hardcoded mappings.
+// In this case, the response field name will be dynamically determined using the value of the "type" field.
+// Ex:
+//
+//	{"type":"data", "data": []}
+//	{"type":"teams", "teams":[]}
+//	{"type":"segments", "segments":[]}
 func extractListFieldName(node *ajson.Node) (string, error) {
 	// default field at which list is stored
 	defaultFieldName := "data"
diff --git a/providers/intercom/read.go b/providers/intercom/read.go
index 60d25145..32ac6b16 100644
--- a/providers/intercom/read.go
+++ b/providers/intercom/read.go
@@ -13,6 +13,10 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	if !supportedObjectsByRead.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
 	url, err := c.buildReadURL(config)
 	if err != nil {
 		return nil, err
diff --git a/providers/pipeliner/objectNames.go b/providers/pipeliner/objectNames.go
new file mode 100644
index 00000000..b2df4148
--- /dev/null
+++ b/providers/pipeliner/objectNames.go
@@ -0,0 +1,11 @@
+package pipeliner
+
+import (
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/providers/pipeliner/metadata"
+)
+
+// Supported object names can be found under schemas.json.
+var supportedObjectsByRead = handy.NewSet( //nolint:gochecknoglobals
+	metadata.Schemas.GetObjectNames(),
+)
diff --git a/providers/pipeliner/read.go b/providers/pipeliner/read.go
index 95c8e93e..95373539 100644
--- a/providers/pipeliner/read.go
+++ b/providers/pipeliner/read.go
@@ -13,6 +13,10 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	if !supportedObjectsByRead.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
 	url, err := c.buildReadURL(config)
 	if err != nil {
 		return nil, err
diff --git a/providers/salesloft/objectNames.go b/providers/salesloft/objectNames.go
new file mode 100644
index 00000000..2647abe6
--- /dev/null
+++ b/providers/salesloft/objectNames.go
@@ -0,0 +1,11 @@
+package salesloft
+
+import (
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/providers/salesloft/metadata"
+)
+
+// Supported object names can be found under schemas.json.
+var supportedObjectsByRead = handy.NewSet( //nolint:gochecknoglobals
+	metadata.Schemas.GetObjectNames(),
+)
diff --git a/providers/salesloft/read.go b/providers/salesloft/read.go
index baf1f478..fbff7dcc 100644
--- a/providers/salesloft/read.go
+++ b/providers/salesloft/read.go
@@ -14,6 +14,10 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	if !supportedObjectsByRead.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
 	url, err := c.buildReadURL(config)
 	if err != nil {
 		return nil, err
diff --git a/providers/smartlead/objectNames.go b/providers/smartlead/objectNames.go
index 3ccba795..34d2f656 100644
--- a/providers/smartlead/objectNames.go
+++ b/providers/smartlead/objectNames.go
@@ -1,6 +1,9 @@
 package smartlead
 
-import "github.com/amp-labs/connectors/common/handy"
+import (
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/providers/smartlead/metadata"
+)
 
 const (
 	objectNameCampaign     = "campaigns"
@@ -8,6 +11,11 @@ const (
 	objectNameClient       = "client"
 )
 
+// Supported object names can be found under schemas.json.
+var supportedObjectsByRead = handy.NewSet( //nolint:gochecknoglobals
+	metadata.Schemas.GetObjectNames(),
+)
+
 var supportedObjectsByWrite = handy.NewSet([]string{ //nolint:gochecknoglobals
 	objectNameCampaign,
 	objectNameEmailAccount,
diff --git a/providers/smartlead/read.go b/providers/smartlead/read.go
index a5f083a0..07cff41c 100644
--- a/providers/smartlead/read.go
+++ b/providers/smartlead/read.go
@@ -11,6 +11,10 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	if !supportedObjectsByRead.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
 	url, err := c.getURL(config.ObjectName)
 	if err != nil {
 		return nil, err
diff --git a/providers/zendesksupport/objectNames.go b/providers/zendesksupport/objectNames.go
new file mode 100644
index 00000000..0d6cece4
--- /dev/null
+++ b/providers/zendesksupport/objectNames.go
@@ -0,0 +1,23 @@
+package zendesksupport
+
+import (
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/providers/zendesksupport/metadata"
+)
+
+// Supported object names can be found under schemas.json.
+var supportedObjectsByRead = handy.NewSet( //nolint:gochecknoglobals
+	metadata.Schemas.GetObjectNames(),
+)
+
+// ObjectNameToResponseField maps ObjectName to the response field name which contains that object.
+var ObjectNameToResponseField = handy.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+	"ticket_audits":        "audits",
+	"search":               "results", // This is "/api/v2/search"
+	"satisfaction_reasons": "reasons",
+},
+	func(key string) string {
+		// Other response fields are named after Object.
+		return key
+	},
+)
diff --git a/providers/zendesksupport/read.go b/providers/zendesksupport/read.go
index bf89de76..1b37bd6f 100644
--- a/providers/zendesksupport/read.go
+++ b/providers/zendesksupport/read.go
@@ -12,6 +12,10 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	if !supportedObjectsByRead.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
 	url, err := c.buildReadURL(config)
 	if err != nil {
 		return nil, err
@@ -22,9 +26,11 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
+	responseFieldName := ObjectNameToResponseField.Get(config.ObjectName)
+
 	return common.ParseResult(
 		rsp,
-		common.GetRecordsUnderJSONPath(config.ObjectName),
+		common.GetRecordsUnderJSONPath(responseFieldName),
 		getNextRecordsURL,
 		common.GetMarshaledData,
 		config.Fields,
diff --git a/scripts/openapi/README.md b/scripts/openapi/README.md
new file mode 100644
index 00000000..60fc5d27
--- /dev/null
+++ b/scripts/openapi/README.md
@@ -0,0 +1,29 @@
+# Description
+
+This folder contains **scripts that process OpenAPI spec to produce Object Schemas**.
+These schemas are later will be served via ListObjectMetadata.
+
+# Structure
+
+Scripts will be located under `scripts/openapi/<PROVIDER_NAME>/metadata/main.go`.
+
+OpenAPI files that it loads can be found under `providers/<PROVIDER_NAME>/openapi/<FILE_NAME>.yaml|json`.
+The output will be saved under `providers/<PROVIDER_NAME>/metadata/schemas.json`.
+
+# Running instructions
+
+Update OpenAPI file to the latest version, then execute the following command in the project root directory:
+
+```
+go run ./scripts/openapi/intercom/metadata
+```
+Check `providers/intercom/metadata/schemas.json` for any side effects. Please monitor the log output,
+and if there are any errors, manually review the OpenAPI spec. Based on your review,
+decide whether the endpoint should be integrated or ignored.
+
+# Capability
+
+These scripts offer:
+* Control over which parts of the OpenAPI spec are relevant for processing.
+* Formatting options for display names.
+* Establishing relationship between Resource/Object Name and JSON field name, containing said object.
diff --git a/scripts/openapi/intercom/metadata/main.go b/scripts/openapi/intercom/metadata/main.go
index 1e7fe54f..761eef89 100644
--- a/scripts/openapi/intercom/metadata/main.go
+++ b/scripts/openapi/intercom/metadata/main.go
@@ -7,6 +7,7 @@ package main
 import (
 	"log/slog"
 
+	"github.com/amp-labs/connectors/providers/intercom"
 	"github.com/amp-labs/connectors/providers/intercom/metadata"
 	"github.com/amp-labs/connectors/providers/intercom/openapi"
 	"github.com/amp-labs/connectors/tools/fileconv/api3"
@@ -32,15 +33,6 @@ var (
 		"news_items":      "News Items",
 		"newsfeeds":       "Newsfeeds",
 	}
-	objectNameToResponseField = map[string]string{ // nolint:gochecknoglobals
-		"admins":        "admins",
-		"teams":         "teams",
-		"ticket_types":  "ticket_types",
-		"events":        "events",
-		"segments":      "segments",
-		"activity_logs": "activity_logs",
-		// the rest uses `data`
-	}
 )
 
 func main() {
@@ -48,7 +40,8 @@ func main() {
 	must(err)
 
 	objects, err := explorer.GetBasicReadObjects(
-		ignoreEndpoints, nil, displayNameOverride, IsResponseFieldAppropriate,
+		ignoreEndpoints, nil, displayNameOverride,
+		api3.CustomMappingObjectCheck(intercom.ObjectNameToResponseField),
 	)
 	must(err)
 
@@ -72,15 +65,6 @@ func main() {
 	slog.Info("Completed.")
 }
 
-func IsResponseFieldAppropriate(objectName, fieldName string) bool {
-	if responseFieldName, ok := objectNameToResponseField[objectName]; ok {
-		return fieldName == responseFieldName
-	}
-
-	// Other objects have items located under `data` response field.
-	return api3.DataObjectCheck(objectName, fieldName)
-}
-
 func must(err error) {
 	if err != nil {
 		panic(err)
diff --git a/scripts/openapi/zendesksupport/metadata/main.go b/scripts/openapi/zendesksupport/metadata/main.go
index dc4f216e..ac11db1f 100644
--- a/scripts/openapi/zendesksupport/metadata/main.go
+++ b/scripts/openapi/zendesksupport/metadata/main.go
@@ -3,6 +3,7 @@ package main
 import (
 	"log/slog"
 
+	"github.com/amp-labs/connectors/providers/zendesksupport"
 	"github.com/amp-labs/connectors/providers/zendesksupport/metadata"
 	"github.com/amp-labs/connectors/providers/zendesksupport/openapi"
 	"github.com/amp-labs/connectors/tools/fileconv/api3"
@@ -67,11 +68,6 @@ var (
 		"satisfaction_reasons": "Satisfaction Rating Reasons",
 		"ticket_audits":        "Ticket Audits",
 	}
-	objectNameToResponseField = map[string]string{ // nolint:gochecknoglobals
-		"ticket_audits":        "audits",
-		"search":               "results", // This is "/api/v2/search"
-		"satisfaction_reasons": "reasons",
-	}
 )
 
 func main() {
@@ -84,7 +80,8 @@ func main() {
 	must(err)
 
 	objects, err := explorer.GetBasicReadObjects(
-		ignoreEndpoints, nil, displayNameOverride, IsResponseFieldAppropriate,
+		ignoreEndpoints, nil, displayNameOverride,
+		api3.CustomMappingObjectCheck(zendesksupport.ObjectNameToResponseField),
 	)
 	must(err)
 
@@ -108,14 +105,6 @@ func main() {
 	slog.Info("Completed.")
 }
 
-func IsResponseFieldAppropriate(objectName, fieldName string) bool {
-	if responseFieldName, ok := objectNameToResponseField[objectName]; ok {
-		return fieldName == responseFieldName
-	}
-
-	return api3.IdenticalObjectCheck(objectName, fieldName)
-}
-
 func must(err error) {
 	if err != nil {
 		panic(err)
diff --git a/tools/fileconv/api3/parameters.go b/tools/fileconv/api3/parameters.go
index 52042753..d751a7b3 100644
--- a/tools/fileconv/api3/parameters.go
+++ b/tools/fileconv/api3/parameters.go
@@ -1,6 +1,7 @@
 package api3
 
 import (
+	"github.com/amp-labs/connectors/common/handy"
 	"github.com/amp-labs/connectors/common/naming"
 	"github.com/iancoleman/strcase"
 )
@@ -21,6 +22,27 @@ func DataObjectCheck(objectName, fieldName string) bool {
 	return fieldName == "data"
 }
 
+// CustomMappingObjectCheck builds ObjectCheck using mapping,
+// which knows exceptions and patterns to determine response field name.
+//
+// Ex:
+//
+//	CustomMappingObjectCheck(handy.NewDefaultMap(map[string]string{
+//			"orders":	"orders",
+//			"carts":	"carts",
+//			"coupons":	"coupons",
+//		}, func(key string) string { return "data" }))
+//
+// This can be understood as follows: orders, carts, coupons REST resources will be found under JSON response field
+// matching "it's name", while the rest will be located under "data" field.
+func CustomMappingObjectCheck(dict *handy.DefaultMap[string, string]) ObjectCheck {
+	return func(objectName, fieldName string) bool {
+		expected := dict.Get(objectName)
+
+		return fieldName == expected
+	}
+}
+
 // DisplayNameProcessor allows to format Display Names.
 type DisplayNameProcessor func(displayName string) string
 


Test patch:
diff --git a/providers/intercom/read_test.go b/providers/intercom/read_test.go
index e421ef06..7d4e3160 100644
--- a/providers/intercom/read_test.go
+++ b/providers/intercom/read_test.go
@@ -46,6 +46,12 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{common.ErrMissingFields},
 		},
+		{
+			Name:         "Unsupported object name",
+			Input:        common.ReadParams{ObjectName: "butterflies", Fields: []string{"id"}},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrOperationNotSupportedForObject},
+		},
 		{
 			Name:         "Mime response header expected",
 			Input:        common.ReadParams{ObjectName: "contacts", Fields: []string{"id"}},
@@ -108,7 +114,8 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 		},
 		{
 			Name:  "API version header is passed as server request",
-			Input: common.ReadParams{ObjectName: "notes", Fields: []string{"id"}},
+			Input: common.ReadParams{ObjectName: "articles", Fields: []string{"id"}},
+			// notes is not supported for now, but its payload is good for testing
 			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				w.Header().Set("Content-Type", "application/json")
 				mockutils.RespondToHeader(w, r, testApiVersionHeader, func() {
@@ -125,7 +132,8 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 		},
 		{
 			Name:  "Next page URL is resolved, when provided with a string",
-			Input: common.ReadParams{ObjectName: "notes", Fields: []string{"id"}},
+			Input: common.ReadParams{ObjectName: "articles", Fields: []string{"id"}},
+			// notes is not supported for now, but its payload is good for testing
 			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				w.Header().Set("Content-Type", "application/json")
 				w.WriteHeader(http.StatusOK)
@@ -159,7 +167,8 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 		},
 		{
 			Name:  "Next page URL is empty, when provided with null object",
-			Input: common.ReadParams{ObjectName: "notes", Fields: []string{"id"}},
+			Input: common.ReadParams{ObjectName: "articles", Fields: []string{"id"}},
+			// notes is not supported for now, but its payload is good for testing
 			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				w.Header().Set("Content-Type", "application/json")
 				w.WriteHeader(http.StatusOK)
diff --git a/providers/pipeliner/read_test.go b/providers/pipeliner/read_test.go
index f964f27f..c2c343c4 100644
--- a/providers/pipeliner/read_test.go
+++ b/providers/pipeliner/read_test.go
@@ -37,6 +37,12 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{common.ErrMissingFields},
 		},
+		{
+			Name:         "Unsupported object name",
+			Input:        common.ReadParams{ObjectName: "butterflies", Fields: []string{"id"}},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrOperationNotSupportedForObject},
+		},
 		{
 			Name:         "Mime response header expected",
 			Input:        common.ReadParams{ObjectName: "Profiles", Fields: []string{"id"}},
diff --git a/providers/salesloft/read_test.go b/providers/salesloft/read_test.go
index 00779a25..12ce5be7 100644
--- a/providers/salesloft/read_test.go
+++ b/providers/salesloft/read_test.go
@@ -42,6 +42,12 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{common.ErrMissingFields},
 		},
+		{
+			Name:         "Unsupported object name",
+			Input:        common.ReadParams{ObjectName: "butterflies", Fields: []string{"id"}},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrOperationNotSupportedForObject},
+		},
 		{
 			Name:         "Mime response header expected",
 			Input:        common.ReadParams{ObjectName: "users", Fields: []string{"id"}},
diff --git a/providers/smartlead/read_test.go b/providers/smartlead/read_test.go
index 4d2b4431..3d68f21d 100644
--- a/providers/smartlead/read_test.go
+++ b/providers/smartlead/read_test.go
@@ -32,19 +32,25 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop
 		},
 		{
 			Name:         "At least one field is requested",
-			Input:        common.ReadParams{ObjectName: "contact"},
+			Input:        common.ReadParams{ObjectName: "email-accounts"},
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{common.ErrMissingFields},
 		},
+		{
+			Name:         "Unsupported object name",
+			Input:        common.ReadParams{ObjectName: "butterflies", Fields: []string{"id"}},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrOperationNotSupportedForObject},
+		},
 		{
 			Name:         "Mime response header expected",
-			Input:        common.ReadParams{ObjectName: "contact", Fields: []string{"id"}},
+			Input:        common.ReadParams{ObjectName: "email-accounts", Fields: []string{"id"}},
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{interpreter.ErrMissingContentType},
 		},
 		{
 			Name:  "Correct error message is understood from HTML response",
-			Input: common.ReadParams{ObjectName: "contact", Fields: []string{"id"}},
+			Input: common.ReadParams{ObjectName: "email-accounts", Fields: []string{"id"}},
 			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				w.Header().Set("Content-Type", "text/html")
 				w.WriteHeader(http.StatusBadRequest)
@@ -57,7 +63,7 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop
 		},
 		{
 			Name:  "Incorrect data type in payload",
-			Input: common.ReadParams{ObjectName: "contact", Fields: []string{"id"}},
+			Input: common.ReadParams{ObjectName: "email-accounts", Fields: []string{"id"}},
 			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				w.Header().Set("Content-Type", "application/json")
 				w.WriteHeader(http.StatusOK)