Repository: amp-labs/connectors
Pull request #72 (2024-02-26 20:27:12)

chore: Update ProviderInfo & fix linter warnings 
### ProviderInfo changes
* Adds the following configuration information to ProviderInfo 
  * `explicitScopesRequired`
  * `explicitWorkspaceRequired`
* Update `Catalog` var to `catalog` and add a `ReadCatalog` function that returns a cloned `catalog` to avoid accidental mutations
* Renames `ReadConfig` to `ReadInfo` wherever applicable
* Add validation over `CatalogType`

### Others
* Fixes linter warnings
* Fixes a bug where a `nil` substitutions variable was causing a panic 



Patch:
diff --git a/README.md b/README.md
index b45874ae..4fad73c4 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ func main() {
   }
 
   // Make a request to Salesforce
-  result, err := client.Read(context.Background(), connectors.ReadConfig{
+  result, err := client.Read(context.Background(), connectors.ReadParams{
     ObjectName: "Contact",
     Fields: []string{"FirstName", "LastName", "Email"},
   })
diff --git a/basic/connector.go b/basic/connector.go
index 156f4beb..fd70bbf6 100644
--- a/basic/connector.go
+++ b/basic/connector.go
@@ -52,7 +52,7 @@ func NewConnector(
 	}
 
 	// Read provider info & replace catalog variables with given substitutions, if any
-	providerInfo, err := providers.ReadConfig(conn.provider, &params.substitutions)
+	providerInfo, err := providers.ReadInfo(conn.provider, &params.substitutions)
 	if err != nil {
 		return nil, err
 	}
diff --git a/go.mod b/go.mod
index 2e20f533..84e513d7 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
-	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 )
 
@@ -28,6 +28,6 @@ require (
 	golang.org/x/net v0.21.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gotest.tools/v3 v3.5.1
 )
diff --git a/go.sum b/go.sum
index 0e7f5489..15b908f0 100644
--- a/go.sum
+++ b/go.sum
@@ -2,7 +2,6 @@ github.com/antchfx/xpath v0.0.0-20170515025933-1f3266e77307 h1:C735MoY/X+UOx6SEC
 github.com/antchfx/xpath v0.0.0-20170515025933-1f3266e77307/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
 github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -20,18 +19,12 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
-github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0=
 github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/subchen/go-xmldom v1.1.2 h1:7evI2YqfYYOnuj+PBwyaOZZYjl3iWq35P6KfBUw9jeU=
@@ -57,7 +50,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
diff --git a/hubspot/connector.go b/hubspot/connector.go
index 918b67e3..aed69897 100644
--- a/hubspot/connector.go
+++ b/hubspot/connector.go
@@ -39,7 +39,7 @@ func NewConnector(opts ...Option) (conn *Connector, outErr error) {
 	}
 
 	// Read provider info & replace catalog variables with given substitutions, if any
-	providerInfo, err := providers.ReadConfig(providers.Hubspot, nil)
+	providerInfo, err := providers.ReadInfo(providers.Hubspot, nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/catalog.go b/providers/catalog.go
index 861ba1fa..7dba8efb 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -14,14 +14,16 @@ const (
 // Contains critical provider configuration (using types from types.gen.go)
 // ================================================================================
 
-var Catalog = CatalogType{ // nolint:gochecknoglobals
+var catalog = CatalogType{ // nolint:gochecknoglobals
 	// Salesforce configuration
 	Salesforce: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.my.salesforce.com",
 		OauthOpts: OauthOpts{
-			AuthURL:  "https://{{.workspace}}.my.salesforce.com/services/oauth2/authorize",
-			TokenURL: "https://{{.workspace}}.my.salesforce.com/services/oauth2/token",
+			AuthURL:                   "https://{{.workspace}}.my.salesforce.com/services/oauth2/authorize",
+			TokenURL:                  "https://{{.workspace}}.my.salesforce.com/services/oauth2/token",
+			ExplicitScopesRequired:    false,
+			ExplicitWorkspaceRequired: true,
 		},
 		Support: Support{
 			BulkWrite: true,
@@ -41,8 +43,10 @@ var Catalog = CatalogType{ // nolint:gochecknoglobals
 		AuthType: Oauth2,
 		BaseURL:  "https://api.hubapi.com",
 		OauthOpts: OauthOpts{
-			AuthURL:  "https://app.hubspot.com/oauth/authorize",
-			TokenURL: "https://api.hubapi.com/oauth/v1/token",
+			AuthURL:                   "https://app.hubspot.com/oauth/authorize",
+			TokenURL:                  "https://api.hubapi.com/oauth/v1/token",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: false,
 		},
 		Support: Support{
 			BulkWrite: false,
@@ -58,8 +62,10 @@ var Catalog = CatalogType{ // nolint:gochecknoglobals
 		AuthType: Oauth2,
 		BaseURL:  "https://api.linkedin.com",
 		OauthOpts: OauthOpts{
-			AuthURL:  "https://www.linkedin.com/oauth/v2/authorization",
-			TokenURL: "https://www.linkedin.com/oauth/v2/accessToken",
+			AuthURL:                   "https://www.linkedin.com/oauth/v2/authorization",
+			TokenURL:                  "https://www.linkedin.com/oauth/v2/accessToken",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: false,
 		},
 		Support: Support{
 			BulkWrite: false,
diff --git a/providers/types.gen.go b/providers/types.gen.go
index 0ad13ecf..7147fe64 100644
--- a/providers/types.gen.go
+++ b/providers/types.gen.go
@@ -16,8 +16,10 @@ type CatalogType map[string]ProviderInfo
 
 // OauthOpts defines model for OauthOpts.
 type OauthOpts struct {
-	AuthURL  string `json:"authURL"`
-	TokenURL string `json:"tokenURL"`
+	AuthURL                   string `json:"authURL" validate:"required"`
+	ExplicitScopesRequired    bool   `json:"explicitScopesRequired"`
+	ExplicitWorkspaceRequired bool   `json:"explicitWorkspaceRequired"`
+	TokenURL                  string `json:"tokenURL" validate:"required"`
 }
 
 // Provider defines model for Provider.
@@ -25,11 +27,11 @@ type Provider = string
 
 // ProviderInfo defines model for ProviderInfo.
 type ProviderInfo struct {
-	AuthType     AuthType     `json:"authType"`
-	BaseURL      string       `json:"baseURL"`
-	OauthOpts    OauthOpts    `json:"oauthOpts"`
+	AuthType     AuthType     `json:"authType" validate:"required"`
+	BaseURL      string       `json:"baseURL" validate:"required"`
+	OauthOpts    OauthOpts    `json:"oauthOpts" validate:"required"`
 	ProviderOpts ProviderOpts `json:"providerOpts"`
-	Support      Support      `json:"support"`
+	Support      Support      `json:"support" validate:"required"`
 }
 
 // ProviderOpts defines model for ProviderOpts.
diff --git a/providers/types.yaml b/providers/types.yaml
index 0fdf005c..90cb53e2 100644
--- a/providers/types.yaml
+++ b/providers/types.yaml
@@ -17,9 +17,13 @@ components:
     AuthType:
       type: string
       enum: [oauth2]
+      x-oapi-codegen-extra-tags:
+        validate: required
 
     Support:
       type: object
+      x-oapi-codegen-extra-tags:
+        validate: required
       required:
         - bulkWrite
         - proxy
@@ -40,16 +44,30 @@ components:
 
     OauthOpts:
       type: object
+      x-oapi-codegen-extra-tags:
+        validate: required
       required:
           - authURL
           - tokenURL
+          - explicitScopesRequired
+          - explicitWorkspaceRequired
       properties:
         authURL:
           type: string
           example: https://login.salesforce.com/services/oauth2/authorize
+          x-oapi-codegen-extra-tags:
+            validate: required
         tokenURL:
           type: string
           example: https://login.salesforce.com/services/oauth2/token
+          x-oapi-codegen-extra-tags:
+            validate: required
+        explicitScopesRequired:
+          type: boolean
+          example: true
+        explicitWorkspaceRequired:
+          type: boolean
+          example: true
 
     ProviderOpts:
         type: object
@@ -73,12 +91,14 @@ components:
           $ref: '#/components/schemas/AuthType'
         baseURL:
           type: string
+          x-oapi-codegen-extra-tags:
+            validate: required
         oauthOpts:
-            $ref: '#/components/schemas/OauthOpts'
+          $ref: '#/components/schemas/OauthOpts'
         support:
-            $ref: '#/components/schemas/Support'
+          $ref: '#/components/schemas/Support'
         providerOpts:
-            $ref: '#/components/schemas/ProviderOpts'
+          $ref: '#/components/schemas/ProviderOpts'
 
     CatalogType:
       type: object
diff --git a/providers/utils.go b/providers/utils.go
index 35cce5ed..7090063d 100644
--- a/providers/utils.go
+++ b/providers/utils.go
@@ -1,6 +1,8 @@
 package providers
 
 import (
+	"bytes"
+	"encoding/gob"
 	"errors"
 	"html/template"
 	"reflect"
@@ -9,29 +11,58 @@ import (
 	"github.com/go-playground/validator"
 )
 
-var ErrProviderCatalogNotFound = errors.New("provider or provider catalog not found")
+var (
+	ErrProviderCatalogNotFound = errors.New("provider or provider catalog not found")
+	ErrProviderOptionNotFound  = errors.New("provider option not found")
+)
 
-// ReadConfig reads the configuration from the catalog for specific provider. It also performs string substitution
+func ReadCatalog() (CatalogType, error) {
+	catalog, err := clone[CatalogType](catalog)
+	if err != nil {
+		return nil, err
+	}
+
+	// Validate the provider configuration
+	v := validator.New()
+	for _, providerInfo := range catalog {
+		if err := v.Struct(providerInfo); err != nil {
+			return nil, err
+		}
+	}
+
+	return catalog, nil
+}
+
+// ReadInfo reads the information from the catalog for specific provider. It also performs string substitution
 // on the values in the config that are surrounded by {{}}.
-func ReadConfig(provider Provider, substitutions *map[string]string) (*ProviderInfo, error) {
-	providerConfig, ok := Catalog[provider]
+func ReadInfo(provider Provider, substitutions *map[string]string) (*ProviderInfo, error) {
+	pInfo, ok := catalog[provider]
 	if !ok {
 		return nil, ErrProviderCatalogNotFound
 	}
 
+	// Clone before modifying
+	providerInfo, err := clone[ProviderInfo](pInfo)
+	if err != nil {
+		return nil, err
+	}
+
 	// Validate the provider configuration
 	v := validator.New()
-	if err := v.Struct(&providerConfig); err != nil {
+	if err := v.Struct(providerInfo); err != nil {
 		return nil, err
 	}
 
+	if substitutions == nil {
+		substitutions = &map[string]string{}
+	}
+
 	// Apply substitutions to the provider configuration values which contain variables in the form of {{var}}.
-	err := substituteStruct(&providerConfig, substitutions)
-	if err != nil {
+	if err := substituteStruct(&providerInfo, substitutions); err != nil {
 		return nil, err
 	}
 
-	return &providerConfig, nil
+	return &providerInfo, nil
 }
 
 // substituteStruct performs string substitution on the fields of the input struct
@@ -105,3 +136,21 @@ func (i *ProviderInfo) GetOption(key string) (string, bool) {
 
 	return val, ok
 }
+
+// clone uses gob to deep copy objects.
+func clone[T any](input T) (T, error) { // nolint:ireturn
+	var buf bytes.Buffer
+	enc := gob.NewEncoder(&buf)
+	dec := gob.NewDecoder(&buf)
+
+	if err := enc.Encode(input); err != nil {
+		return input, err
+	}
+
+	var clone T
+	if err := dec.Decode(&clone); err != nil {
+		return input, err
+	}
+
+	return clone, nil
+}
diff --git a/salesforce/connector.go b/salesforce/connector.go
index 5c73e583..1117ff39 100644
--- a/salesforce/connector.go
+++ b/salesforce/connector.go
@@ -8,7 +8,7 @@ import (
 )
 
 const (
-	providerOptionRestApiUrl = "restApiUrl"
+	providerOptionRestApiURL = "restApiUrl"
 	providerOptionDomain     = "domain"
 )
 
@@ -59,21 +59,21 @@ func NewConnector(opts ...Option) (conn *Connector, outErr error) {
 	}
 
 	// Read provider info & replace catalog variables with given substitutions, if any
-	providerInfo, err := providers.ReadConfig(providers.Salesforce, &map[string]string{
+	providerInfo, err := providers.ReadInfo(providers.Salesforce, &map[string]string{
 		"workspace": params.workspace,
 	})
 	if err != nil {
 		return nil, err
 	}
 
-	restApi, ok := providerInfo.GetOption(providerOptionRestApiUrl)
+	restApi, ok := providerInfo.GetOption(providerOptionRestApiURL)
 	if !ok {
-		return nil, fmt.Errorf("missing restApiUrl option in provider info")
+		return nil, fmt.Errorf("restApiUrl not set: %w", providers.ErrProviderOptionNotFound)
 	}
 
 	domain, ok := providerInfo.GetOption(providerOptionDomain)
 	if !ok {
-		return nil, fmt.Errorf("missing domain option in provider info")
+		return nil, fmt.Errorf("domain not set: %w", providers.ErrProviderOptionNotFound)
 	}
 
 	conn = &Connector{
diff --git a/scripts/oauth/token.go b/scripts/oauth/token.go
index 7de1e1c7..025e868c 100644
--- a/scripts/oauth/token.go
+++ b/scripts/oauth/token.go
@@ -1,3 +1,4 @@
+// nolint
 package main
 
 import (
@@ -267,7 +268,7 @@ func setup() *OAuthApp {
 		substitutionsMap[key] = val.MustString()
 	}
 
-	providerInfo, err := providers.ReadConfig(provider, &substitutionsMap)
+	providerInfo, err := providers.ReadInfo(provider, &substitutionsMap)
 	if err != nil {
 		slog.Error("failed to read provider config", "error", err)
 
diff --git a/scripts/proxy/proxy.go b/scripts/proxy/proxy.go
index 8525e532..1049f572 100644
--- a/scripts/proxy/proxy.go
+++ b/scripts/proxy/proxy.go
@@ -1,8 +1,8 @@
+// nolint
 package main
 
 import (
 	"context"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"log/slog"
@@ -117,6 +117,7 @@ func main() {
 	for key, val := range substitutions {
 		substitutionsMap[key] = val.MustString()
 	}
+
 	validateRequiredFlags(provider, clientId, clientSecret)
 	startProxy(provider, oauthScopes, clientId, clientSecret, substitutionsMap, DefaultPort, accessToken, refreshToken)
 }
@@ -134,6 +135,7 @@ func startProxy(provider string, scopes []string, clientId, clientSecret string,
 	http.Handle("/", proxy)
 
 	fmt.Printf("\nProxy server listening on :%d\n", port)
+
 	if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil { // nosemgrep
 		panic(err)
 	}
@@ -153,7 +155,7 @@ func buildProxy(provider string, scopes []string, clientId, clientSecret string,
 }
 
 func getProviderConfig(provider string, substitutions map[string]string) *providers.ProviderInfo {
-	config, err := providers.ReadConfig(provider, &substitutions)
+	config, err := providers.ReadInfo(provider, &substitutions)
 	if err != nil {
 		panic(err)
 	}
@@ -161,19 +163,6 @@ func getProviderConfig(provider string, substitutions map[string]string) *provid
 	return config
 }
 
-func readToken(tokenPath string) *oauth2.Token {
-	data, err := os.ReadFile(tokenPath)
-	if err != nil {
-		panic(err)
-	}
-
-	var token oauth2.Token
-	if err := json.Unmarshal(data, &token); err != nil {
-		panic(err)
-	}
-	return &token
-}
-
 func configureOAuth(clientId, clientSecret string, scopes []string, providerInfo *providers.ProviderInfo) *oauth2.Config {
 	return &oauth2.Config{
 		ClientID:     clientId,
@@ -190,8 +179,9 @@ func configureOAuth(clientId, clientSecret string, scopes []string, providerInfo
 // This helps with refreshing tokens automatically.
 func setupHttpClient(cfg *oauth2.Config, accessToken, refreshToken string, provider string, providerInfo *providers.ProviderInfo) *http.Client {
 	ctx := context.Background()
+
 	conn, err := basic.NewConnector(
-		providers.Provider(provider),
+		provider,
 		basic.WithClient(ctx, http.DefaultClient, cfg, &oauth2.Token{AccessToken: accessToken, RefreshToken: refreshToken}),
 	)
 	if err != nil {
@@ -237,24 +227,3 @@ type customTransport struct {
 func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	return t.httpClient.Do(req)
 }
-
-func convertSubstitutions(sub string) map[string]string {
-	if len(sub) == 0 {
-		return nil
-	}
-
-	parts := strings.Split(sub, ",")
-
-	substitutions := make(map[string]string)
-
-	for _, part := range parts {
-		parts := strings.Split(part, "=")
-		if len(parts) != 2 {
-			continue
-		}
-
-		substitutions[parts[0]] = parts[1]
-	}
-
-	return substitutions
-}


Test patch:
diff --git a/providers/catalog_test.go b/providers/catalog_test.go
index d21595cb..6b4c497c 100644
--- a/providers/catalog_test.go
+++ b/providers/catalog_test.go
@@ -30,8 +30,10 @@ var testCases = []struct { // nolint
 			},
 			AuthType: Oauth2,
 			OauthOpts: OauthOpts{
-				AuthURL:  "https://example.my.salesforce.com/services/oauth2/authorize",
-				TokenURL: "https://example.my.salesforce.com/services/oauth2/token",
+				AuthURL:                   "https://example.my.salesforce.com/services/oauth2/authorize",
+				TokenURL:                  "https://example.my.salesforce.com/services/oauth2/token",
+				ExplicitWorkspaceRequired: true,
+				ExplicitScopesRequired:    false,
 			},
 			BaseURL: "https://example.my.salesforce.com",
 			ProviderOpts: ProviderOpts{
@@ -57,8 +59,10 @@ var testCases = []struct { // nolint
 			},
 			AuthType: Oauth2,
 			OauthOpts: OauthOpts{
-				AuthURL:  "https://app.hubspot.com/oauth/authorize",
-				TokenURL: "https://api.hubapi.com/oauth/v1/token",
+				AuthURL:                   "https://app.hubspot.com/oauth/authorize",
+				TokenURL:                  "https://api.hubapi.com/oauth/v1/token",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: false,
 			},
 			BaseURL: "https://api.hubapi.com",
 		},
@@ -80,8 +84,10 @@ var testCases = []struct { // nolint
 			},
 			AuthType: Oauth2,
 			OauthOpts: OauthOpts{
-				AuthURL:  "https://www.linkedin.com/oauth/v2/authorization",
-				TokenURL: "https://www.linkedin.com/oauth/v2/accessToken",
+				AuthURL:                   "https://www.linkedin.com/oauth/v2/authorization",
+				TokenURL:                  "https://www.linkedin.com/oauth/v2/accessToken",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: false,
 			},
 			BaseURL: "https://api.linkedin.com",
 		},
@@ -98,7 +104,7 @@ var testCases = []struct { // nolint
 	},
 }
 
-func TestReadConfig(t *testing.T) { // nolint
+func TestReadInfo(t *testing.T) { // nolint
 	t.Parallel()
 
 	for _, tc := range testCases {
@@ -107,7 +113,7 @@ func TestReadConfig(t *testing.T) { // nolint
 		t.Run(tc.provider, func(t *testing.T) {
 			t.Parallel()
 
-			config, err := ReadConfig(tc.provider, &tc.substitutions)
+			config, err := ReadInfo(tc.provider, &tc.substitutions)
 
 			if !errors.Is(err, tc.expectedErr) {
 				t.Errorf("[%s] Expected error: %v, but got: %v", tc.description, tc.expectedErr, err)
diff --git a/test/hubspot/main.go b/test/hubspot/main.go
index b0cdde33..556058c7 100644
--- a/test/hubspot/main.go
+++ b/test/hubspot/main.go
@@ -2,9 +2,11 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"os/signal"
 	"syscall"
+	"time"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/test/utils"
@@ -71,4 +73,19 @@ func main() {
 
 	// Dump the result.
 	utils.DumpJSON(result, os.Stdout)
+	fmt.Println("Wrote contact")
+
+	res, err := hsConn.Read(ctx, common.ReadParams{
+		ObjectName: "contacts",
+		Fields:     []string{"email", "phone", "company", "website", "lastname", "firstname"},
+		NextPage:   "",
+		Since:      time.Now().Add(-5 * time.Minute),
+	})
+	if err != nil {
+		utils.Fail("error reading from hubspot", "error", err)
+	}
+
+	fmt.Println("Reading contacts..")
+	// Dump the result.
+	utils.DumpJSON(res, os.Stdout)
 }