Repository: amp-labs/connectors
Pull request #1811 (2025-06-25 00:24:40)

[CON-1620,CON-1621] feat(atlassian): getModuleURL
# Live tests

- [x] auth-metadata
![image](https://github.com/user-attachments/assets/b77d6bb6-53d4-4579-bbca-a44f1184fbfb)
- [x] metadata
![image](https://github.com/user-attachments/assets/066aaf59-431a-459b-aea5-6c4ea2618be2)
- [x] read
![image](https://github.com/user-attachments/assets/b827c240-9769-4a2d-a3d6-f19a920a9783)
- [x] write-delete
![image](https://github.com/user-attachments/assets/a341efb4-6836-4a27-ad3f-6e3f5c6d6f47)

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


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->

Patch:
diff --git a/common/urlbuilder/url.go b/common/urlbuilder/url.go
index c9af4306..cd06fd5f 100644
--- a/common/urlbuilder/url.go
+++ b/common/urlbuilder/url.go
@@ -100,6 +100,14 @@ func (u *URL) Path() string {
 	return u.delegate.Path
 }
 
+func (u *URL) Origin() string {
+	if u.delegate.Scheme == "" || u.delegate.Host == "" {
+		return ""
+	}
+
+	return u.delegate.Scheme + "://" + u.delegate.Host
+}
+
 func (u *URL) String() string {
 	// Everything stays the same
 	// The only thing that we alter in the delegate's query params
diff --git a/providers/atlassian.go b/providers/atlassian.go
index 6a3e05da..d2615b1a 100644
--- a/providers/atlassian.go
+++ b/providers/atlassian.go
@@ -81,6 +81,9 @@ func init() {
 					Name:        "workspace",
 					DisplayName: "App name",
 					DocsURL:     "https://support.atlassian.com/organization-administration/docs/update-your-product-and-site-url/",
+					ModuleDependencies: &ModuleDependencies{
+						ModuleAtlassianJiraConnect: {},
+					},
 				},
 			},
 		},
diff --git a/providers/atlassian/authmetadata.go b/providers/atlassian/authmetadata.go
index 0507d9b0..69afcec1 100644
--- a/providers/atlassian/authmetadata.go
+++ b/providers/atlassian/authmetadata.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 
 	"github.com/amp-labs/connectors/common"
-	"github.com/amp-labs/connectors/common/urlbuilder"
 	"github.com/amp-labs/connectors/internal/jsonquery"
 )
 
@@ -20,8 +19,6 @@ func (c *Connector) GetPostAuthInfo(ctx context.Context) (*common.PostAuthInfo,
 		return nil, errors.Join(ErrDiscoveryFailure, err)
 	}
 
-	c.cloudId = cloudId
-
 	return &common.PostAuthInfo{
 		CatalogVars: AuthMetadataVars{
 			CloudId: cloudId,
@@ -92,9 +89,3 @@ func (c *Connector) retrieveCloudId(ctx context.Context) (string, error) {
 	// Hence, we couldn't resolve cloud id.
 	return "", ErrContainerNotFound
 }
-
-// URL allows to get list of sites associated with auth token.
-// https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site
-func (c *Connector) getAccessibleSitesURL() (*urlbuilder.URL, error) {
-	return urlbuilder.New("https://api.atlassian.com", "oauth/token/accessible-resources")
-}
diff --git a/providers/atlassian/authmetamodel.go b/providers/atlassian/authmetamodel.go
index 6ce0d471..ae397ee4 100644
--- a/providers/atlassian/authmetamodel.go
+++ b/providers/atlassian/authmetamodel.go
@@ -1,5 +1,9 @@
 package atlassian
 
+import (
+	"github.com/amp-labs/connectors/common/substitutions/catalogreplacer"
+)
+
 const cloudIdKey = "cloudId"
 
 // AuthMetadataVars is a complete list of authentication metadata associated with connector.
@@ -21,3 +25,10 @@ func (v AuthMetadataVars) AsMap() *map[string]string {
 		cloudIdKey: v.CloudId,
 	}
 }
+
+func (v AuthMetadataVars) GetSubstitutionPlan() catalogreplacer.SubstitutionPlan {
+	return catalogreplacer.SubstitutionPlan{
+		From: "cloudId",
+		To:   v.CloudId,
+	}
+}
diff --git a/providers/atlassian/connector.go b/providers/atlassian/connector.go
index 860217fd..cbc39b69 100644
--- a/providers/atlassian/connector.go
+++ b/providers/atlassian/connector.go
@@ -1,31 +1,26 @@
 package atlassian
 
 import (
-	"errors"
 	"fmt"
-	"strings"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/interpreter"
 	"github.com/amp-labs/connectors/common/paramsbuilder"
-	"github.com/amp-labs/connectors/common/substitutions/catalogreplacer"
 	"github.com/amp-labs/connectors/common/urlbuilder"
 	"github.com/amp-labs/connectors/providers"
 )
 
-// ErrMissingCloudId happens when cloud id was not provided via WithMetadata.
-var ErrMissingCloudId = errors.New("connector missing cloud id")
+const apiVersion = "3"
 
 type Connector struct {
-	Client     *common.JSONHTTPClient
-	moduleInfo *providers.ModuleInfo
-	moduleID   common.ModuleID
+	Client *common.JSONHTTPClient
 
 	// workspace is used to find cloud ID.
 	workspace string
-	cloudId   string
 
-	*providers.ProviderInfo
+	providerInfo *providers.ProviderInfo
+	moduleInfo   *providers.ModuleInfo
+	moduleID     common.ModuleID
 }
 
 func NewConnector(opts ...Option) (conn *Connector, outErr error) {
@@ -47,18 +42,19 @@ func NewConnector(opts ...Option) (conn *Connector, outErr error) {
 
 	// Convert metadata map to model.
 	authMetadata := NewAuthMetadataVars(params.Metadata.Map)
-	conn.cloudId = authMetadata.CloudId
 
-	if err := conn.setProviderInfo(); err != nil {
+	conn.providerInfo, err = providers.ReadInfo(conn.Provider(), &params.Workspace, authMetadata)
+	if err != nil {
 		return nil, err
 	}
 
-	conn.moduleInfo = conn.ProviderInfo.ReadModuleInfo(conn.moduleID)
+	conn.moduleInfo = conn.providerInfo.ReadModuleInfo(conn.moduleID)
 
-	// connector and its client must mirror base url and provide its own error parser
-	conn.setBaseURL(conn.BaseURL)
+	// HTTPClient will soon not store Base URL.
+	conn.Client.HTTPClient.Base = conn.providerInfo.BaseURL
 	conn.Client.HTTPClient.ErrorHandler = interpreter.ErrorHandler{
 		JSON: interpreter.NewFaultyResponder(errorFormats, nil),
+		HTML: &interpreter.DirectFaultyResponder{Callback: conn.interpretHTMLError},
 	}.Handle
 
 	return conn, nil
@@ -72,68 +68,27 @@ func (c *Connector) String() string {
 	return fmt.Sprintf("%s.Connector[%s]", c.Provider(), c.moduleID)
 }
 
-// URL format follows structure applicable to Oauth2 Atlassian apps.
-// https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#other-integrations
-func (c *Connector) getJiraRestApiURL(arg string) (*urlbuilder.URL, error) {
-	// In the case of JIRA / Atlassian Cloud, we use this path. In other cases, we fall back to the base path.
-	modulePath := supportedModules[c.moduleID].Path()
-
-	if c.moduleID == providers.ModuleAtlassianJira {
-		cloudId, err := c.getCloudId()
-		if err != nil {
-			return nil, err
-		}
-
-		return urlbuilder.New(c.BaseURL, "ex/jira", cloudId, modulePath, arg)
-	}
-
-	return urlbuilder.New(c.BaseURL, modulePath, arg)
-}
-
-func (c *Connector) setBaseURL(newURL string) {
-	// This is a temporary fix. And will be addressed when URLs are properly loaded from ProviderInfo.
-	if strings.Contains(newURL, "ex/jira") {
-		newURL = "https://api.atlassian.com"
-	}
-
-	c.BaseURL = newURL
-	c.Client.HTTPClient.Base = newURL
-}
-
-func (c *Connector) getCloudId() (string, error) {
-	if len(c.cloudId) == 0 {
-		return "", ErrMissingCloudId
-	}
-
-	return c.cloudId, nil
+// This method must be used only by the unit tests.
+func (c *Connector) setBaseURL(rootURL, moduleURL string) {
+	c.providerInfo.BaseURL = rootURL
+	c.moduleInfo.BaseURL = moduleURL
 }
 
-func (c *Connector) setProviderInfo() error {
-	// Read provider info
-	providerInfo, err := providers.ReadInfo(c.Provider())
+// URL allows to get list of sites associated with auth token.
+// https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site
+func (c *Connector) getAccessibleSitesURL() (*urlbuilder.URL, error) {
+	url, err := urlbuilder.New(c.providerInfo.BaseURL)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	c.ProviderInfo = providerInfo
-
-	// When the module is Atlassian Connect, the base URL is different, so we need to override it.
-	// TODO: Replace options with substitution map in the future to avoid having to know
-	// which values need to be substituted.
-	if c.moduleID == providers.ModuleAtlassianJiraConnect {
-		vars := []catalogreplacer.CatalogVariable{
-			&paramsbuilder.Workspace{Name: c.workspace},
-		}
-
-		override := &providers.ProviderInfo{
-			BaseURL: "https://{{.workspace}}.atlassian.net",
-		}
+	return urlbuilder.New(url.Origin(), "oauth/token/accessible-resources")
+}
 
-		// Mutates the provider info with the overrides, and substitutes any variables.
-		if err := c.ProviderInfo.Override(override).SubstituteWith(vars); err != nil {
-			return err
-		}
-	}
+// URL format for providers.ModuleAtlassianJira follows structure applicable to Oauth2 Atlassian apps:
+// https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#other-integrations
+func (c *Connector) getModuleURL(path ...string) (*urlbuilder.URL, error) {
+	path = append([]string{apiVersion}, path...)
 
-	return nil
+	return urlbuilder.New(c.moduleInfo.BaseURL, path...)
 }
diff --git a/providers/atlassian/delete.go b/providers/atlassian/delete.go
index 7a322ed3..88c98615 100644
--- a/providers/atlassian/delete.go
+++ b/providers/atlassian/delete.go
@@ -12,7 +12,7 @@ func (c *Connector) Delete(ctx context.Context, config common.DeleteParams) (*co
 		return nil, err
 	}
 
-	url, err := c.getJiraRestApiURL("issue")
+	url, err := c.getModuleURL("issue")
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/atlassian/errors.go b/providers/atlassian/errors.go
index 52669b31..5c6618e3 100644
--- a/providers/atlassian/errors.go
+++ b/providers/atlassian/errors.go
@@ -1,11 +1,14 @@
 package atlassian
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
+	"net/http"
 	"strings"
 	"time"
 
+	"github.com/PuerkitoBio/goquery"
 	"github.com/amp-labs/connectors/common/interpreter"
 )
 
@@ -78,3 +81,18 @@ func (r ResponseStatusError) CombineErr(base error) error {
 
 	return fmt.Errorf("%w: %v - %v", base, r.Error, r.Message)
 }
+
+func (c *Connector) interpretHTMLError(res *http.Response, body []byte) error {
+	base := interpreter.DefaultStatusCodeMappingToErr(res, body)
+
+	document, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
+	if err != nil {
+		// ignore HTML that cannot be understood
+		return base
+	}
+
+	// Message is located under the <pre></pre> tag
+	message := document.Find("head > title").Text()
+
+	return fmt.Errorf("%w: %v", base, message)
+}
diff --git a/providers/atlassian/metadata.go b/providers/atlassian/metadata.go
index 03222cea..60e76844 100644
--- a/providers/atlassian/metadata.go
+++ b/providers/atlassian/metadata.go
@@ -12,7 +12,7 @@ import (
 // API Reference:
 // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-fields/#api-rest-api-2-field-get
 func (c *Connector) ListObjectMetadata(ctx context.Context, _ []string) (*common.ListObjectMetadataResult, error) {
-	url, err := c.getJiraRestApiURL("field")
+	url, err := c.getModuleURL("field")
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/atlassian/modules.go b/providers/atlassian/modules.go
index ee7429bb..58a8dcae 100644
--- a/providers/atlassian/modules.go
+++ b/providers/atlassian/modules.go
@@ -5,17 +5,7 @@ import (
 	"github.com/amp-labs/connectors/providers"
 )
 
-const (
-	// ModuleJira
-	// Deprecated.
-	ModuleJira = providers.ModuleAtlassianJira
-	// ModuleAtlassianJiraConnect
-	// Deprecated.
-	ModuleAtlassianJiraConnect = providers.ModuleAtlassianJiraConnect
-)
-
-// supportedModules represents currently working and supported modules within the Atlassian connector.
-// Any added module should be appended here.
+// Deprecated.
 var supportedModules = common.Modules{ // nolint: gochecknoglobals
 	common.ModuleRoot: {
 		ID:      common.ModuleRoot,
diff --git a/providers/atlassian/read.go b/providers/atlassian/read.go
index eccc1032..ffa201cc 100644
--- a/providers/atlassian/read.go
+++ b/providers/atlassian/read.go
@@ -39,7 +39,7 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 }
 
 func (c *Connector) buildReadURL(config common.ReadParams) (*urlbuilder.URL, error) {
-	url, err := c.getJiraRestApiURL("search")
+	url, err := c.getModuleURL("search")
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/atlassian/write.go b/providers/atlassian/write.go
index 52519249..29818f6d 100644
--- a/providers/atlassian/write.go
+++ b/providers/atlassian/write.go
@@ -18,7 +18,7 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm
 		return nil, err
 	}
 
-	url, err := c.getJiraRestApiURL("issue")
+	url, err := c.getModuleURL("issue")
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/utils.go b/providers/utils.go
index 9f539499..a07a832c 100644
--- a/providers/utils.go
+++ b/providers/utils.go
@@ -159,22 +159,22 @@ func (c CustomCatalog) ReadInfo(provider Provider, vars ...catalogreplacer.Catal
 func (i *ProviderInfo) SubstituteWith(vars catalogreplacer.CatalogVariables) error {
 	// Take care of default metadata values.
 	if i.Metadata != nil {
-		for _, metadatInput := range i.Metadata.Input {
-			if metadatInput.DefaultValue != "" {
+		for _, metadataInput := range i.Metadata.Input {
+			if metadataInput.DefaultValue != "" {
 				vars.AddDefaults(catalogreplacer.CustomCatalogVariable{Plan: catalogreplacer.SubstitutionPlan{
-					From: metadatInput.Name,
-					To:   metadatInput.DefaultValue,
+					From: metadataInput.Name,
+					To:   metadataInput.DefaultValue,
 				}})
 			}
 		}
 
 		// To prevent OAuthConnect from erroring out due to missing PostAuthentication variables,
-		// we add a default value of " " for each PostAuthentication variable.
+		// we add a default value of "" for each PostAuthentication variable.
 		// Since no Connection exists yet, there won't be any PostAuthentication variables.
 		for _, postAuthVar := range i.Metadata.PostAuthentication {
 			vars.AddDefaults(catalogreplacer.CustomCatalogVariable{Plan: catalogreplacer.SubstitutionPlan{
 				From: postAuthVar.Name,
-				To:   " ",
+				To:   "",
 			}})
 		}
 	}


Test patch:
diff --git a/common/urlbuilder/url_test.go b/common/urlbuilder/url_test.go
index 8bbd6297..5b6523bc 100644
--- a/common/urlbuilder/url_test.go
+++ b/common/urlbuilder/url_test.go
@@ -364,3 +364,54 @@ func TestEquality(t *testing.T) { // nolint:funlen
 		})
 	}
 }
+
+func TestURLOrigin(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "HTTP with port",
+			input:    "http://example.com:8080/path?query=1",
+			expected: "http://example.com:8080",
+		},
+		{
+			name:     "HTTPS without port",
+			input:    "https://example.com/some/path",
+			expected: "https://example.com",
+		},
+		{
+			name:     "HTTPS with subdomain",
+			input:    "https://sub.example.com/abc",
+			expected: "https://sub.example.com",
+		},
+		{
+			name:     "HTTP with IP address",
+			input:    "http://127.0.0.1:3000/test",
+			expected: "http://127.0.0.1:3000",
+		},
+		{
+			name:     "Empty URL",
+			input:    "",
+			expected: "",
+		},
+	}
+
+	for _, tt := range tests { // nolint:varnamelen
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			endpoint, err := url.Parse(tt.input)
+			if err != nil {
+				t.Fatalf("%s: is an invalid test, check input", tt.name)
+			}
+
+			outputURL, err := FromRawURL(endpoint)
+			output := outputURL.Origin()
+			testutils.CheckOutputWithError(t, tt.name, tt.expected, nil, output, err)
+		})
+	}
+}
diff --git a/providers/atlassian/authmetadata_test.go b/providers/atlassian/authmetadata_test.go
index 2e8260a0..7bc005c6 100644
--- a/providers/atlassian/authmetadata_test.go
+++ b/providers/atlassian/authmetadata_test.go
@@ -1,103 +1,123 @@
 package atlassian
 
-// func TestGetPostAuthInfo(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
-// 	t.Parallel()
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"strings"
+	"testing"
 
-// 	responseCloudID := testutils.DataFromFile(t, "cloud-id.json")
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/test/utils/mockutils"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
+	"github.com/amp-labs/connectors/test/utils/testutils"
+	"github.com/go-test/deep"
+)
 
-// 	tests := []struct {
-// 		name         string
-// 		server       *httptest.Server
-// 		expected     *common.PostAuthInfo
-// 		expectedErrs []error
-// 	}{
-// 		{
-// 			name: "Response should be an array",
-// 			server: mockserver.Fixed{
-// 				Setup:  mockserver.ContentJSON(),
-// 				Always: mockserver.ResponseString(http.StatusOK, `{}`),
-// 			}.Server(),
-// 			expectedErrs: []error{
-// 				ErrDiscoveryFailure,
-// 			},
-// 		},
-// 		{
-// 			name: "Empty container list, missing cloud id",
-// 			server: mockserver.Fixed{
-// 				Setup:  mockserver.ContentJSON(),
-// 				Always: mockserver.ResponseString(http.StatusOK, `[]`),
-// 			}.Server(),
-// 			expectedErrs: []error{
-// 				ErrContainerNotFound,
-// 				ErrDiscoveryFailure,
-// 			},
-// 		},
-// 		{
-// 			name: "Workspace is matched against container, success locating cloud id",
-// 			server: mockserver.Fixed{
-// 				Setup: mockserver.ContentJSON(),
-// 				// response file has workspace that we set up in the constructor
-// 				Always: mockserver.Response(http.StatusOK, responseCloudID),
-// 			}.Server(),
-// 			expected: &common.PostAuthInfo{
-// 				CatalogVars: &map[string]string{
-// 					"cloudId": "ebc887b2-7e61-4059-ab35-71f15cc16e12",
-// 				},
-// 				RawResponse: nil,
-// 			},
-// 			expectedErrs: nil,
-// 		},
-// 	}
+func TestGetPostAuthInfo(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
+	t.Parallel()
 
-// 	for _, tt := range tests { // nolint:varnamelen
-// 		// nolint:varnamelen
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			t.Parallel()
+	responseCloudID := testutils.DataFromFile(t, "cloud-id.json")
 
-// 			defer tt.server.Close()
+	tests := []struct {
+		name         string
+		server       *httptest.Server
+		expected     *common.PostAuthInfo
+		expectedErrs []error
+	}{
+		{
+			name: "Response should be an array",
+			server: mockserver.Fixed{
+				Setup:  mockserver.ContentJSON(),
+				Always: mockserver.ResponseString(http.StatusOK, `{}`),
+			}.Server(),
+			expectedErrs: []error{
+				ErrDiscoveryFailure,
+			},
+		},
+		{
+			name: "Empty container list, missing cloud id",
+			server: mockserver.Fixed{
+				Setup:  mockserver.ContentJSON(),
+				Always: mockserver.ResponseString(http.StatusOK, `[]`),
+			}.Server(),
+			expectedErrs: []error{
+				ErrContainerNotFound,
+				ErrDiscoveryFailure,
+			},
+		},
+		{
+			name: "Workspace is matched against container, success locating cloud id",
+			server: mockserver.Fixed{
+				Setup: mockserver.ContentJSON(),
+				// response file has workspace that we set up in the constructor
+				Always: mockserver.Response(http.StatusOK, responseCloudID),
+			}.Server(),
+			expected: &common.PostAuthInfo{
+				CatalogVars: &map[string]string{
+					"cloudId": "ebc887b2-7e61-4059-ab35-71f15cc16e12",
+				},
+				RawResponse: nil,
+			},
+			expectedErrs: nil,
+		},
+	}
 
-// 			ctx := context.Background()
+	for _, tt := range tests { // nolint:varnamelen
+		// nolint:varnamelen
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
 
-// 			connector, err := NewConnector(
-// 				WithAuthenticatedClient(mockutils.NewClient()),
-// 				WithWorkspace("second-proj"),
-// 				WithModule(providers.ModuleAtlassianJira),
-// 			)
-// 			if err != nil {
-// 				t.Fatalf("%s: error in test while constructing connector %v", tt.name, err)
-// 			}
+			defer tt.server.Close()
 
-// 			// for testing we want to redirect calls to our mock server
-// 			connector.setBaseURL(tt.server.URL)
+			ctx := context.Background()
 
-// 			if err != nil {
-// 				t.Fatalf("%s: failed to setup auth metadata connector %v", tt.name, err)
-// 			}
+			connector, err := NewConnector(
+				WithAuthenticatedClient(mockutils.NewClient()),
+				WithWorkspace("second-proj"),
+				WithModule(providers.ModuleAtlassianJira),
+			)
+			if err != nil {
+				t.Fatalf("%s: error in test while constructing connector %v", tt.name, err)
+			}
 
-// 			// start of tests
-// 			output, err := connector.GetPostAuthInfo(ctx)
-// 			if err != nil {
-// 				if len(tt.expectedErrs) == 0 {
-// 					t.Fatalf("%s: expected no errors, got: (%v)", tt.name, err)
-// 				}
-// 			} else {
-// 				// check that missing error is what is expected
-// 				if len(tt.expectedErrs) != 0 {
-// 					t.Fatalf("%s: expected errors (%v), but got nothing", tt.name, tt.expectedErrs)
-// 				}
-// 			}
+			// for testing we want to redirect calls to our mock server
+			connector.setBaseURL(
+				mockutils.ReplaceURLOrigin(connector.providerInfo.BaseURL, tt.server.URL),
+				mockutils.ReplaceURLOrigin(connector.moduleInfo.BaseURL, tt.server.URL),
+			)
 
-// 			// check every error
-// 			for _, expectedErr := range tt.expectedErrs {
-// 				if !errors.Is(err, expectedErr) && !strings.Contains(err.Error(), expectedErr.Error()) {
-// 					t.Fatalf("%s: expected Error: (%v), got: (%v)", tt.name, expectedErr, err)
-// 				}
-// 			}
+			if err != nil {
+				t.Fatalf("%s: failed to setup auth metadata connector %v", tt.name, err)
+			}
 
-// 			if !reflect.DeepEqual(output, tt.expected) {
-// 				diff := deep.Equal(output, tt.expected)
-// 				t.Fatalf("%s:, \nexpected: (%v), \ngot: (%v), \ndiff: (%v)", tt.name, tt.expected, output, diff)
-// 			}
-// 		})
-// 	}
-// }
+			// start of tests
+			output, err := connector.GetPostAuthInfo(ctx)
+			if err != nil {
+				if len(tt.expectedErrs) == 0 {
+					t.Fatalf("%s: expected no errors, got: (%v)", tt.name, err)
+				}
+			} else {
+				// check that missing error is what is expected
+				if len(tt.expectedErrs) != 0 {
+					t.Fatalf("%s: expected errors (%v), but got nothing", tt.name, tt.expectedErrs)
+				}
+			}
+
+			// check every error
+			for _, expectedErr := range tt.expectedErrs {
+				if !errors.Is(err, expectedErr) && !strings.Contains(err.Error(), expectedErr.Error()) {
+					t.Fatalf("%s: expected Error: (%v), got: (%v)", tt.name, expectedErr, err)
+				}
+			}
+
+			if !reflect.DeepEqual(output, tt.expected) {
+				diff := deep.Equal(output, tt.expected)
+				t.Fatalf("%s:, \nexpected: (%v), \ngot: (%v), \ndiff: (%v)", tt.name, tt.expected, output, diff)
+			}
+		})
+	}
+}
diff --git a/providers/atlassian/delete_test.go b/providers/atlassian/delete_test.go
index 14d43817..096e2314 100644
--- a/providers/atlassian/delete_test.go
+++ b/providers/atlassian/delete_test.go
@@ -1,15 +1,12 @@
 package atlassian
 
 import (
-	"context"
 	"errors"
 	"net/http"
 	"testing"
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
-	"github.com/amp-labs/connectors/providers"
-	"github.com/amp-labs/connectors/test/utils/mockutils"
 	"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
 	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
 	"github.com/amp-labs/connectors/test/utils/testroutines"
@@ -72,21 +69,3 @@ func TestDelete(t *testing.T) { // nolint:funlen,cyclop
 		})
 	}
 }
-
-func TestDeleteWithoutMetadata(t *testing.T) {
-	t.Parallel()
-
-	connector, err := NewConnector(
-		WithAuthenticatedClient(mockutils.NewClient()),
-		WithWorkspace("test-workspace"),
-		WithModule(providers.ModuleAtlassianJira),
-	)
-	if err != nil {
-		t.Fatal("failed to create connector")
-	}
-
-	_, err = connector.Delete(context.Background(), common.DeleteParams{ObjectName: "issues", RecordId: "123"})
-	if !errors.Is(err, ErrMissingCloudId) {
-		t.Fatalf("expected Delete method to complain about missing cloud id")
-	}
-}
diff --git a/providers/atlassian/metadata_test.go b/providers/atlassian/metadata_test.go
index 981913aa..a7b24708 100644
--- a/providers/atlassian/metadata_test.go
+++ b/providers/atlassian/metadata_test.go
@@ -1,15 +1,11 @@
 package atlassian
 
 import (
-	"context"
-	"errors"
 	"net/http"
 	"testing"
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
-	"github.com/amp-labs/connectors/providers"
-	"github.com/amp-labs/connectors/test/utils/mockutils"
 	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
 	"github.com/amp-labs/connectors/test/utils/testroutines"
 	"github.com/amp-labs/connectors/test/utils/testutils"
@@ -108,21 +104,3 @@ func TestListObjectMetadata(t *testing.T) { // nolint:funlen,gocognit,cyclop
 		})
 	}
 }
-
-func TestListObjectMetadataWithoutMetadata(t *testing.T) {
-	t.Parallel()
-
-	connector, err := NewConnector(
-		WithAuthenticatedClient(mockutils.NewClient()),
-		WithWorkspace("test-workspace"),
-		WithModule(providers.ModuleAtlassianJira),
-	)
-	if err != nil {
-		t.Fatal("failed to create connector")
-	}
-
-	_, err = connector.ListObjectMetadata(context.Background(), nil)
-	if !errors.Is(err, ErrMissingCloudId) {
-		t.Fatalf("expected ListObjectMetadata method to complain about missing cloud id")
-	}
-}
diff --git a/providers/atlassian/read_test.go b/providers/atlassian/read_test.go
index 14515023..f4a93dd2 100644
--- a/providers/atlassian/read_test.go
+++ b/providers/atlassian/read_test.go
@@ -1,7 +1,6 @@
 package atlassian
 
 import (
-	"context"
 	"errors"
 	"net/http"
 	"testing"
@@ -263,27 +262,6 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 	}
 }
 
-func TestReadWithoutMetadata(t *testing.T) {
-	t.Parallel()
-
-	connector, err := NewConnector(
-		WithAuthenticatedClient(mockutils.NewClient()),
-		WithWorkspace("test-workspace"),
-		WithModule(providers.ModuleAtlassianJira),
-	)
-	if err != nil {
-		t.Fatal("failed to create connector")
-	}
-
-	_, err = connector.Read(context.Background(), common.ReadParams{
-		ObjectName: "issues",
-		Fields:     connectors.Fields("id"),
-	})
-	if !errors.Is(err, ErrMissingCloudId) {
-		t.Fatalf("expected Read method to complain about missing cloud id")
-	}
-}
-
 func constructTestConnector(serverURL string) (*Connector, error) {
 	connector, err := NewConnector(
 		WithAuthenticatedClient(mockutils.NewClient()),
@@ -297,8 +275,10 @@ func constructTestConnector(serverURL string) (*Connector, error) {
 		return nil, err
 	}
 
-	// for testing we want to redirect calls to our mock server
-	connector.setBaseURL(mockutils.ReplaceURLOrigin(connector.HTTPClient().Base, serverURL))
+	connector.setBaseURL(
+		mockutils.ReplaceURLOrigin(connector.providerInfo.BaseURL, serverURL),
+		mockutils.ReplaceURLOrigin(connector.moduleInfo.BaseURL, serverURL),
+	)
 
 	return connector, nil
 }
diff --git a/providers/atlassian/write_test.go b/providers/atlassian/write_test.go
index 7c93daea..63680f24 100644
--- a/providers/atlassian/write_test.go
+++ b/providers/atlassian/write_test.go
@@ -1,15 +1,12 @@
 package atlassian
 
 import (
-	"context"
 	"errors"
 	"net/http"
 	"testing"
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
-	"github.com/amp-labs/connectors/providers"
-	"github.com/amp-labs/connectors/test/utils/mockutils"
 	"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
 	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
 	"github.com/amp-labs/connectors/test/utils/testroutines"
@@ -111,21 +108,3 @@ func TestWrite(t *testing.T) { // nolint:funlen,cyclop
 		})
 	}
 }
-
-func TestWriteWithoutMetadata(t *testing.T) {
-	t.Parallel()
-
-	connector, err := NewConnector(
-		WithAuthenticatedClient(mockutils.NewClient()),
-		WithWorkspace("test-workspace"),
-		WithModule(providers.ModuleAtlassianJira),
-	)
-	if err != nil {
-		t.Fatal("failed to create connector")
-	}
-
-	_, err = connector.Write(context.Background(), common.WriteParams{ObjectName: "issues", RecordData: "dummy"})
-	if !errors.Is(err, ErrMissingCloudId) {
-		t.Fatalf("expected Write method to complain about missing cloud id")
-	}
-}
diff --git a/test/atlassian/connector.go b/test/atlassian/connector.go
index 7faddde4..98474e8d 100644
--- a/test/atlassian/connector.go
+++ b/test/atlassian/connector.go
@@ -22,6 +22,28 @@ func GetAtlassianConnector(ctx context.Context) *atlassian.Connector {
 	return makeAtlassianConnector(ctx, providers.ModuleAtlassianJira)
 }
 
+func makeAtlassianConnector(ctx context.Context, module common.ModuleID) *atlassian.Connector {
+	filePath := credscanning.LoadPath(providers.Atlassian)
+	reader := utils.MustCreateProvCredJSON(filePath, true, fieldCloudID)
+
+	conn, err := atlassian.NewConnector(
+		atlassian.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
+		atlassian.WithWorkspace(reader.Get(credscanning.Fields.Workspace)),
+		atlassian.WithModule(module),
+		atlassian.WithMetadata(map[string]string{
+			// This value can be obtained by following this API reference.
+			// https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site
+			// Another simplest solution is to run `connectors/test/atlassian/auth-metadata/main.go` script.
+			"cloudId": reader.Get(fieldCloudID),
+		}),
+	)
+	if err != nil {
+		utils.Fail("error creating connector", "error", err)
+	}
+
+	return conn
+}
+
 // GetAtlassianConnectConnector
 // Context:
 // https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/
@@ -75,25 +97,3 @@ func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
 
 	return cfg
 }
-
-func makeAtlassianConnector(ctx context.Context, module common.ModuleID) *atlassian.Connector {
-	filePath := credscanning.LoadPath(providers.Atlassian)
-	reader := utils.MustCreateProvCredJSON(filePath, true, fieldCloudID)
-
-	conn, err := atlassian.NewConnector(
-		atlassian.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
-		atlassian.WithWorkspace(reader.Get(credscanning.Fields.Workspace)),
-		atlassian.WithModule(module),
-		atlassian.WithMetadata(map[string]string{
-			// This value can be obtained by following this API reference.
-			// https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site
-			// Another simplest solution is to run `connectors/test/atlassian/auth-metadata/main.go` script.
-			"cloudId": reader.Get(fieldCloudID),
-		}),
-	)
-	if err != nil {
-		utils.Fail("error creating connector", "error", err)
-	}
-
-	return conn
-}
diff --git a/test/atlassian/jwt/main.go b/test/atlassian/jwt/main.go
index 6539faa5..7cf05694 100644
--- a/test/atlassian/jwt/main.go
+++ b/test/atlassian/jwt/main.go
@@ -2,11 +2,12 @@ package main
 
 import (
 	"context"
-	"fmt"
+	"os"
 	"time"
 
 	"github.com/amp-labs/connectors"
 	connTest "github.com/amp-labs/connectors/test/atlassian"
+	"github.com/amp-labs/connectors/test/utils"
 )
 
 func main() {
@@ -18,12 +19,13 @@ func main() {
 
 	// Use conn
 	res, err := conn.Read(ctx, connectors.ReadParams{
-		Fields: connectors.Fields("id", "project", "description"),
-		Since:  time.Now().Add(-time.Hour * 24),
+		ObjectName: "issue",
+		Fields:     connectors.Fields("id", "project", "description"),
+		Since:      time.Now().Add(-time.Hour * 24),
 	})
 	if err != nil {
-		panic(err)
+		utils.Fail("error reading", "error", err)
 	}
 
-	fmt.Println(res)
+	utils.DumpJSON(res, os.Stdout)
 }
diff --git a/test/atlassian/metadata/main.go b/test/atlassian/metadata/main.go
index 11f4b34d..69ace7b3 100644
--- a/test/atlassian/metadata/main.go
+++ b/test/atlassian/metadata/main.go
@@ -2,20 +2,15 @@ package main
 
 import (
 	"context"
-	"log/slog"
+	"fmt"
+	"os"
 	"os/signal"
 	"syscall"
 
-	"github.com/amp-labs/connectors/common"
 	connTest "github.com/amp-labs/connectors/test/atlassian"
 	"github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/test/utils/mockutils"
 )
 
-var objectName = "issue" // nolint: gochecknoglobals
-
-// We want to compare fields returned by read and schema properties provided by metadata methods.
-// Properties from read must all be present in schema definition.
 func main() {
 	// Handle Ctrl-C gracefully.
 	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
@@ -26,26 +21,13 @@ func main() {
 
 	conn := connTest.GetAtlassianConnector(ctx)
 
-	response, err := conn.Read(ctx, common.ReadParams{})
-	if err != nil {
-		utils.Fail("error reading from Atlassian", "error", err)
-	}
-
-	if response.Rows == 0 {
-		utils.Fail("expected to read at least one record", "error", err)
-	}
-
-	metadata, err := conn.ListObjectMetadata(ctx, nil)
+	metadata, err := conn.ListObjectMetadata(ctx, []string{
+		"issue",
+	})
 	if err != nil {
-		utils.Fail("error listing metadata for Atlassian", "error", err)
+		utils.Fail("error listing metadata", "error", err)
 	}
 
-	slog.Info("Compare object metadata against endpoint response:")
-
-	mismatchErr := mockutils.ValidateReadConformsMetadata(objectName, response.Data[0].Raw, metadata)
-	if mismatchErr != nil {
-		utils.Fail("schema and payload response have mismatching fields", "error", mismatchErr)
-	} else {
-		slog.Info("==> success fields match.")
-	}
+	fmt.Println("Metadata...")
+	utils.DumpJSON(metadata, os.Stdout)
 }
diff --git a/test/atlassian/write-delete/main.go b/test/atlassian/write-delete/main.go
index 80792aa4..ed31c10a 100644
--- a/test/atlassian/write-delete/main.go
+++ b/test/atlassian/write-delete/main.go
@@ -45,7 +45,7 @@ func main() {
 	newTitle := "Fix button dropdown in main menu"
 
 	testscenario.ValidateCreateUpdateDelete(ctx, conn,
-		"issues",
+		"issue",
 		issuePayload{
 			Fields: issueFields{
 				Project: identifier{