diff --git a/providers/hubspot/internal/core/objects.go b/providers/hubspot/internal/core/objects.go index a57d4c82e..60df60af6 100644 --- a/providers/hubspot/internal/core/objects.go +++ b/providers/hubspot/internal/core/objects.go @@ -1,6 +1,9 @@ package core -import "github.com/amp-labs/connectors/internal/datautils" +import ( + "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/internal/datautils" +) const ( // DefaultPageSize is the default page size for paginated requests. @@ -28,7 +31,33 @@ var ( // MarketingObjects contains object names that belong to the HubSpot Marketing API. // // The Marketing API is separate from the CRM API and is not related to the Objects API. - MarketingObjects = datautils.NewSet( - "campaigns", - ) + MarketingObjects = datautils.Map[string, ObjectDescription]{ + "campaigns": { + Path: "campaigns", + RecordTransformer: common.FlattenNestedFields("properties"), + }, + // "marketing/emails" refers to HubSpot marketing emails, which are distinct + // from the CRM email activity resource. + // + // The object name preserves the marketing-prefixed endpoint form to avoid a + // naming collision with CRM emails. + // + // Path is relative to the Marketing API base path. + // + // Marketing emails: + // https://developers.hubspot.com/docs/api-reference/latest/marketing/marketing-emails/get-emails + // CRM emails: + // https://developers.hubspot.com/docs/api-reference/latest/crm/activities/emails/guide + "marketing/emails": { + Path: "emails", + RecordTransformer: nil, // None. Fields and Raw are the same. + }, + } ) + +type ObjectDescription struct { + // Path is URL path segment. + Path string + // RecordTransformer describes how to convert raw response and then extract selected fields by read operation. + RecordTransformer common.RecordTransformer +} diff --git a/providers/hubspot/internal/metadata/schemas.json b/providers/hubspot/internal/metadata/schemas.json index 1a541021e..df857a702 100755 --- a/providers/hubspot/internal/metadata/schemas.json +++ b/providers/hubspot/internal/metadata/schemas.json @@ -157,6 +157,528 @@ "providerType": "Monetary value" } } + }, + "marketing/emails": { + "displayName": "Marketing Emails", + "description": "/marketing/emails/2026-03", + "responseKey": "results", + "fields": { + "isAb": { + "displayName": "isAb", + "valueType": "boolean", + "providerType": "boolean" + }, + "activeDomain": { + "displayName": "activeDomain", + "valueType": "string", + "providerType": "string" + }, + "allEmailCampaignIds": { + "displayName": "allEmailCampaignIds", + "valueType": "other", + "providerType": "string[]" + }, + "archived": { + "displayName": "archived", + "valueType": "boolean", + "providerType": "boolean" + }, + "businessUnitId": { + "displayName": "businessUnitId", + "valueType": "string", + "providerType": "string" + }, + "campaign": { + "displayName": "campaign", + "valueType": "string", + "providerType": "string" + }, + "campaignName": { + "displayName": "campaignName", + "valueType": "string", + "providerType": "string" + }, + "campaignUtm": { + "displayName": "campaignUtm", + "valueType": "string", + "providerType": "string" + }, + "clonedFrom": { + "displayName": "clonedFrom", + "valueType": "string", + "providerType": "string" + }, + "content": { + "displayName": "content", + "valueType": "other", + "providerType": "object" + }, + "createdAt": { + "displayName": "createdAt", + "valueType": "datetime", + "providerType": "string" + }, + "createdById": { + "displayName": "createdById", + "valueType": "string", + "providerType": "string" + }, + "deletedAt": { + "displayName": "deletedAt", + "valueType": "datetime", + "providerType": "string" + }, + "emailCampaignGroupId": { + "displayName": "emailCampaignGroupId", + "valueType": "string", + "providerType": "string" + }, + "emailTemplateMode": { + "displayName": "emailTemplateMode", + "valueType": "singleSelect", + "providerType": "enum", + "values": [ + { + "value": "DESIGN_MANAGER", + "displayValue": "DESIGN_MANAGER" + }, + { + "value": "DRAG_AND_DROP", + "displayValue": "DRAG_AND_DROP" + } + ] + }, + "feedbackSurveyId": { + "displayName": "feedbackSurveyId", + "valueType": "string", + "providerType": "string" + }, + "folderId": { + "displayName": "folderId", + "valueType": "int", + "providerType": "integer" + }, + "folderIdV2": { + "displayName": "folderIdV2", + "valueType": "int", + "providerType": "integer" + }, + "from": { + "displayName": "from", + "valueType": "other", + "providerType": "object" + }, + "id": { + "displayName": "id", + "valueType": "string", + "providerType": "string" + }, + "isPublished": { + "displayName": "isPublished", + "valueType": "boolean", + "providerType": "boolean" + }, + "isTransactional": { + "displayName": "isTransactional", + "valueType": "boolean", + "providerType": "boolean" + }, + "jitterSendTime": { + "displayName": "jitterSendTime", + "valueType": "boolean", + "providerType": "boolean" + }, + "language": { + "displayName": "language", + "valueType": "singleSelect", + "providerType": "enum" + }, + "name": { + "displayName": "name", + "valueType": "string", + "providerType": "string" + }, + "previewKey": { + "displayName": "previewKey", + "valueType": "string", + "providerType": "string" + }, + "primaryEmailCampaignId": { + "displayName": "primaryEmailCampaignId", + "valueType": "string", + "providerType": "string" + }, + "publishDate": { + "displayName": "publishDate", + "valueType": "datetime", + "providerType": "string" + }, + "publishedAt": { + "displayName": "publishedAt", + "valueType": "datetime", + "providerType": "string" + }, + "publishedByEmail": { + "displayName": "publishedByEmail", + "valueType": "string", + "providerType": "string" + }, + "publishedById": { + "displayName": "publishedById", + "valueType": "string", + "providerType": "string" + }, + "publishedByName": { + "displayName": "publishedByName", + "valueType": "string", + "providerType": "string" + }, + "rssData": { + "displayName": "rssData", + "valueType": "other", + "providerType": "object" + }, + "sendOnPublish": { + "displayName": "sendOnPublish", + "valueType": "boolean", + "providerType": "boolean" + }, + "state": { + "displayName": "state", + "valueType": "singleSelect", + "providerType": "enum", + "values": [ + { + "value": "AGENT_GENERATED", + "displayValue": "AGENT_GENERATED" + }, + { + "value": "AUTOMATED", + "displayValue": "AUTOMATED" + }, + { + "value": "AUTOMATED_AB", + "displayValue": "AUTOMATED_AB" + }, + { + "value": "AUTOMATED_AB_VARIANT", + "displayValue": "AUTOMATED_AB_VARIANT" + }, + { + "value": "AUTOMATED_DRAFT", + "displayValue": "AUTOMATED_DRAFT" + }, + { + "value": "AUTOMATED_DRAFT_AB", + "displayValue": "AUTOMATED_DRAFT_AB" + }, + { + "value": "AUTOMATED_DRAFT_ABVARIANT", + "displayValue": "AUTOMATED_DRAFT_ABVARIANT" + }, + { + "value": "AUTOMATED_FOR_FORM", + "displayValue": "AUTOMATED_FOR_FORM" + }, + { + "value": "AUTOMATED_FOR_FORM_BUFFER", + "displayValue": "AUTOMATED_FOR_FORM_BUFFER" + }, + { + "value": "AUTOMATED_FOR_FORM_DRAFT", + "displayValue": "AUTOMATED_FOR_FORM_DRAFT" + }, + { + "value": "AUTOMATED_FOR_FORM_LEGACY", + "displayValue": "AUTOMATED_FOR_FORM_LEGACY" + }, + { + "value": "AUTOMATED_LOSER_ABVARIANT", + "displayValue": "AUTOMATED_LOSER_ABVARIANT" + }, + { + "value": "AUTOMATED_SENDING", + "displayValue": "AUTOMATED_SENDING" + }, + { + "value": "BLOG_EMAIL_DRAFT", + "displayValue": "BLOG_EMAIL_DRAFT" + }, + { + "value": "BLOG_EMAIL_PUBLISHED", + "displayValue": "BLOG_EMAIL_PUBLISHED" + }, + { + "value": "DRAFT", + "displayValue": "DRAFT" + }, + { + "value": "DRAFT_AB", + "displayValue": "DRAFT_AB" + }, + { + "value": "DRAFT_AB_VARIANT", + "displayValue": "DRAFT_AB_VARIANT" + }, + { + "value": "ERROR", + "displayValue": "ERROR" + }, + { + "value": "LOSER_AB_VARIANT", + "displayValue": "LOSER_AB_VARIANT" + }, + { + "value": "PAGE_STUB", + "displayValue": "PAGE_STUB" + }, + { + "value": "PRE_PROCESSING", + "displayValue": "PRE_PROCESSING" + }, + { + "value": "PROCESSING", + "displayValue": "PROCESSING" + }, + { + "value": "PUBLISHED", + "displayValue": "PUBLISHED" + }, + { + "value": "PUBLISHED_AB", + "displayValue": "PUBLISHED_AB" + }, + { + "value": "PUBLISHED_AB_VARIANT", + "displayValue": "PUBLISHED_AB_VARIANT" + }, + { + "value": "PUBLISHED_OR_SCHEDULED", + "displayValue": "PUBLISHED_OR_SCHEDULED" + }, + { + "value": "RSS_TO_EMAIL_DRAFT", + "displayValue": "RSS_TO_EMAIL_DRAFT" + }, + { + "value": "RSS_TO_EMAIL_PUBLISHED", + "displayValue": "RSS_TO_EMAIL_PUBLISHED" + }, + { + "value": "SCHEDULED", + "displayValue": "SCHEDULED" + }, + { + "value": "SCHEDULED_AB", + "displayValue": "SCHEDULED_AB" + }, + { + "value": "SCHEDULED_OR_PUBLISHED", + "displayValue": "SCHEDULED_OR_PUBLISHED" + } + ] + }, + "type": { + "displayName": "type", + "valueType": "singleSelect", + "providerType": "enum", + "values": [ + { + "value": "AB_EMAIL", + "displayValue": "AB_EMAIL" + }, + { + "value": "AUTOMATED_AB_EMAIL", + "displayValue": "AUTOMATED_AB_EMAIL" + }, + { + "value": "AUTOMATED_EMAIL", + "displayValue": "AUTOMATED_EMAIL" + }, + { + "value": "BATCH_EMAIL", + "displayValue": "BATCH_EMAIL" + }, + { + "value": "BLOG_EMAIL", + "displayValue": "BLOG_EMAIL" + }, + { + "value": "BLOG_EMAIL_CHILD", + "displayValue": "BLOG_EMAIL_CHILD" + }, + { + "value": "FEEDBACK_CES_EMAIL", + "displayValue": "FEEDBACK_CES_EMAIL" + }, + { + "value": "FEEDBACK_CUSTOM_EMAIL", + "displayValue": "FEEDBACK_CUSTOM_EMAIL" + }, + { + "value": "FEEDBACK_CUSTOM_SURVEY_EMAIL", + "displayValue": "FEEDBACK_CUSTOM_SURVEY_EMAIL" + }, + { + "value": "FEEDBACK_NPS_EMAIL", + "displayValue": "FEEDBACK_NPS_EMAIL" + }, + { + "value": "FOLLOWUP_EMAIL", + "displayValue": "FOLLOWUP_EMAIL" + }, + { + "value": "LEADFLOW_EMAIL", + "displayValue": "LEADFLOW_EMAIL" + }, + { + "value": "LOCALTIME_EMAIL", + "displayValue": "LOCALTIME_EMAIL" + }, + { + "value": "MANAGE_PREFERENCES_EMAIL", + "displayValue": "MANAGE_PREFERENCES_EMAIL" + }, + { + "value": "MARKETING_SINGLE_SEND_API", + "displayValue": "MARKETING_SINGLE_SEND_API" + }, + { + "value": "MEMBERSHIP_EMAIL_VERIFICATION_EMAIL", + "displayValue": "MEMBERSHIP_EMAIL_VERIFICATION_EMAIL" + }, + { + "value": "MEMBERSHIP_FOLLOW_UP_EMAIL", + "displayValue": "MEMBERSHIP_FOLLOW_UP_EMAIL" + }, + { + "value": "MEMBERSHIP_OTP_LOGIN_EMAIL", + "displayValue": "MEMBERSHIP_OTP_LOGIN_EMAIL" + }, + { + "value": "MEMBERSHIP_PASSWORD_RESET_EMAIL", + "displayValue": "MEMBERSHIP_PASSWORD_RESET_EMAIL" + }, + { + "value": "MEMBERSHIP_PASSWORD_SAVED_EMAIL", + "displayValue": "MEMBERSHIP_PASSWORD_SAVED_EMAIL" + }, + { + "value": "MEMBERSHIP_PASSWORDLESS_AUTH_EMAIL", + "displayValue": "MEMBERSHIP_PASSWORDLESS_AUTH_EMAIL" + }, + { + "value": "MEMBERSHIP_REGISTRATION_EMAIL", + "displayValue": "MEMBERSHIP_REGISTRATION_EMAIL" + }, + { + "value": "MEMBERSHIP_REGISTRATION_FOLLOW_UP_EMAIL", + "displayValue": "MEMBERSHIP_REGISTRATION_FOLLOW_UP_EMAIL" + }, + { + "value": "MEMBERSHIP_VERIFICATION_EMAIL", + "displayValue": "MEMBERSHIP_VERIFICATION_EMAIL" + }, + { + "value": "OPTIN_EMAIL", + "displayValue": "OPTIN_EMAIL" + }, + { + "value": "OPTIN_FOLLOWUP_EMAIL", + "displayValue": "OPTIN_FOLLOWUP_EMAIL" + }, + { + "value": "RESUBSCRIBE_EMAIL", + "displayValue": "RESUBSCRIBE_EMAIL" + }, + { + "value": "RSS_EMAIL", + "displayValue": "RSS_EMAIL" + }, + { + "value": "RSS_EMAIL_CHILD", + "displayValue": "RSS_EMAIL_CHILD" + }, + { + "value": "SINGLE_SEND_API", + "displayValue": "SINGLE_SEND_API" + }, + { + "value": "SMTP_TOKEN", + "displayValue": "SMTP_TOKEN" + }, + { + "value": "TICKET_EMAIL", + "displayValue": "TICKET_EMAIL" + } + ] + }, + "stats": { + "displayName": "stats", + "valueType": "other", + "providerType": "object" + }, + "subcategory": { + "displayName": "subcategory", + "valueType": "string", + "providerType": "string" + }, + "subject": { + "displayName": "subject", + "valueType": "string", + "providerType": "string" + }, + "subscriptionDetails": { + "displayName": "subscriptionDetails", + "valueType": "other", + "providerType": "object" + }, + "teamsWithAccess": { + "displayName": "teamsWithAccess", + "valueType": "other", + "providerType": "string[]" + }, + "testing": { + "displayName": "testing", + "valueType": "other", + "providerType": "object" + }, + "to": { + "displayName": "to", + "valueType": "other", + "providerType": "object" + }, + "unpublishedAt": { + "displayName": "unpublishedAt", + "valueType": "datetime", + "providerType": "string" + }, + "updatedAt": { + "displayName": "updatedAt", + "valueType": "datetime", + "providerType": "string" + }, + "updatedById": { + "displayName": "updatedById", + "valueType": "string", + "providerType": "string" + }, + "usersWithAccess": { + "displayName": "usersWithAccess", + "valueType": "other", + "providerType": "string[]" + }, + "webversion": { + "displayName": "webversion", + "valueType": "other", + "providerType": "object" + }, + "workflowNames": { + "displayName": "workflowNames", + "valueType": "other", + "providerType": "string[]" + } + } } } } diff --git a/providers/hubspot/metadata_test.go b/providers/hubspot/metadata_test.go index ee9d89552..5b8c7011d 100644 --- a/providers/hubspot/metadata_test.go +++ b/providers/hubspot/metadata_test.go @@ -371,8 +371,8 @@ func TestListObjectMetadata(t *testing.T) { // nolint:funlen,gocognit,cyclop,mai ExpectedErrs: nil, }, { - Name: "Successfully describe marketing campaigns", - Input: []string{"campaigns"}, + Name: "Successfully describe marketing campaigns and emails", + Input: []string{"campaigns", "marketing/emails"}, Server: mockserver.Dummy(), Comparator: testroutines.ComparatorSubsetMetadata, Expected: &common.ListObjectMetadataResult{ @@ -408,6 +408,21 @@ func TestListObjectMetadata(t *testing.T) { // nolint:funlen,gocognit,cyclop,mai }, }, }, + "marketing/emails": { + DisplayName: "Marketing Emails", + Fields: map[string]common.FieldMetadata{ + "campaignName": { + DisplayName: "campaignName", + ValueType: "string", + ProviderType: "string", + }, + "subject": { + DisplayName: "subject", + ValueType: "string", + ProviderType: "string", + }, + }, + }, }, Errors: nil, }, diff --git a/providers/hubspot/read.go b/providers/hubspot/read.go index a434e7fb5..7b98cb5a4 100644 --- a/providers/hubspot/read.go +++ b/providers/hubspot/read.go @@ -39,7 +39,7 @@ func (c *Connector) Read(ctx context.Context, params common.ReadParams) (*common }) case core.MarketingObjects.Has(params.ObjectName): // Object is part of Hubspot Marketing API. - return c.readMarketing(ctx, params) + return c.readMarketing(ctx, params, core.MarketingObjects[params.ObjectName]) default: // Otherwise object belongs to Hubspot Objects API (sub-category of CRM namespace). return c.readCRMObjectsAPI(ctx, params) @@ -130,8 +130,10 @@ func (c *Connector) buildReadURL(params common.ReadParams) (string, error) { return url.String(), nil } -func (c *Connector) readMarketing(ctx context.Context, params common.ReadParams) (*common.ReadResult, error) { - url, err := c.buildMarketingReadURL(params) +func (c *Connector) readMarketing(ctx context.Context, + params common.ReadParams, object core.ObjectDescription, +) (*common.ReadResult, error) { + url, err := c.buildMarketingReadURL(params, object.Path) if err != nil { return nil, err } @@ -147,7 +149,7 @@ func (c *Connector) readMarketing(ctx context.Context, params common.ReadParams) common.MakeRecordsFunc("results"), makeIncrementalFilterFunc(params), readhelper.MakeMarshaledDataFuncWithId( - common.FlattenNestedFields("properties"), + object.RecordTransformer, readhelper.IdFieldQuery{Field: "id"}, ), params.Fields, @@ -161,14 +163,14 @@ func (c *Connector) readMarketing(ctx context.Context, params common.ReadParams) // https://developers.hubspot.com/docs/api-reference/latest/marketing/campaigns/get-campaigns // - Incremental reading is not available. // - Sorting is applied using "updatedAt" field from newest to oldest. -func (c *Connector) buildMarketingReadURL(params common.ReadParams) (*urlbuilder.URL, error) { +func (c *Connector) buildMarketingReadURL(params common.ReadParams, objectPath string) (*urlbuilder.URL, error) { if len(params.NextPage) != 0 { // Next page return urlbuilder.New(params.NextPage.String()) } // First page - url, err := c.getMarketingURL(params.ObjectName) + url, err := c.getMarketingURL(objectPath) if err != nil { return nil, err } diff --git a/providers/hubspot/read_test.go b/providers/hubspot/read_test.go index c14aed93a..63150fd5e 100644 --- a/providers/hubspot/read_test.go +++ b/providers/hubspot/read_test.go @@ -24,6 +24,8 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx responseListsLast := testutils.DataFromFile(t, "read-lists-2-second-page.json") responseCampaignsFirst := testutils.DataFromFile(t, "read/campaigns/1-first-page.json") responseCampaignsLast := testutils.DataFromFile(t, "read/campaigns/2-last-page.json") + responseMarketingEmailFirst := testutils.DataFromFile(t, "read/marketing-emails/1-first-page.json") + responseMarketingEmailLast := testutils.DataFromFile(t, "read/marketing-emails/2-last-page.json") tests := []testroutines.Read{ { @@ -370,6 +372,88 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx Done: true, }, }, + { + Name: "Read marketing emails first page", + Input: common.ReadParams{ + ObjectName: "marketing/emails", + Fields: connectors.Fields("subject"), + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodGET(), + mockcond.Path("/marketing/emails/2026-03"), + mockcond.QueryParam("sort", "-updatedAt"), + }, + Then: mockserver.Response(http.StatusOK, responseMarketingEmailFirst), + }.Server(), + Comparator: testroutines.ComparatorSubsetRead, + Expected: &common.ReadResult{ + Rows: 2, + Data: []common.ReadResultRow{{ + Fields: map[string]any{ + "subject": "Behold the latest version of our newsletter!", + }, + Raw: map[string]any{ + "createdAt": "2026-05-07T22:59:00.597Z", + "createdById": "82226790", + "emailTemplateMode": "DRAG_AND_DROP", + }, + Id: "212476546342", + }, { + Fields: map[string]any{ + "subject": "Product Launch", + }, + Raw: map[string]any{ + "createdAt": "2024-05-29T22:37:35.474Z", + "createdById": "62365053", + "emailTemplateMode": "DRAG_AND_DROP", + }, + Id: "168871137104", + }}, + NextPage: "https://api.hubapi.com/marketing/emails/2026-03?limit=3&sort=-updatedAt&after=Mw%3D%3D", + Done: false, + }, + }, + { + Name: "Read marketing emails last page", + Input: common.ReadParams{ + ObjectName: "marketing/emails", + Fields: connectors.Fields("subject"), + NextPage: testroutines.URLTestServer + "/marketing/emails/2026-03?limit=3&sort=-updatedAt&after=Mw%3D%3D", + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodGET(), + mockcond.Path("/marketing/emails/2026-03"), + mockcond.QueryParam("limit", "3"), + mockcond.QueryParam("after", "Mw=="), + mockcond.QueryParam("sort", "-updatedAt"), + }, + Then: mockserver.Response(http.StatusOK, responseMarketingEmailLast), + }.Server(), + Comparator: testroutines.ComparatorSubsetRead, + Expected: &common.ReadResult{ + Rows: 1, + Data: []common.ReadResultRow{ + { + Fields: map[string]any{ + "subject": "Your ticket '{{ticket.subject}}' has been received", + }, + Raw: map[string]any{ + "createdAt": "2023-12-08T17:47:58.334Z", + "createdById": "100", + "emailCampaignGroupId": "285768335", + "emailTemplateMode": "DRAG_AND_DROP", + }, + Id: "149139108889", + }, + }, + NextPage: "", + Done: true, + }, + }, } for _, tt := range tests { diff --git a/providers/hubspot/test/read/marketing-emails/1-first-page.json b/providers/hubspot/test/read/marketing-emails/1-first-page.json new file mode 100644 index 000000000..2eb99585a --- /dev/null +++ b/providers/hubspot/test/read/marketing-emails/1-first-page.json @@ -0,0 +1,69 @@ +{ + "total": 3, + "results": [ + { + "activeDomain": "44623425.hs-sites.com", + "archived": false, + "businessUnitId": "0", + "content": {}, + "createdAt": "2026-05-07T22:59:00.597Z", + "createdById": "82226790", + "emailTemplateMode": "DRAG_AND_DROP", + "from": {}, + "id": "212476546342", + "isAb": false, + "isPublished": false, + "isTransactional": false, + "jitterSendTime": true, + "language": "en", + "name": "A new marketing email", + "previewKey": "OqmGJCWC", + "publishDate": "2026-05-07T22:59:00Z", + "sendOnPublish": true, + "state": "DRAFT", + "subcategory": "batch", + "subject": "Behold the latest version of our newsletter!", + "subscriptionDetails": {}, + "to": {}, + "type": "BATCH_EMAIL", + "updatedAt": "2026-05-07T22:59:00.597Z", + "updatedById": "82226790", + "webversion": {} + }, + { + "activeDomain": "44623425.hs-sites.com", + "archived": false, + "businessUnitId": "0", + "content": {}, + "createdAt": "2024-05-29T22:37:35.474Z", + "createdById": "62365053", + "emailTemplateMode": "DRAG_AND_DROP", + "from": {}, + "id": "168871137104", + "isAb": false, + "isPublished": false, + "isTransactional": false, + "jitterSendTime": true, + "language": "en", + "name": "Product Launch", + "previewKey": "XRbaCfJY", + "publishDate": "2024-05-29T22:38:43Z", + "sendOnPublish": true, + "state": "DRAFT", + "subcategory": "batch", + "subject": "Product Launch", + "subscriptionDetails": {}, + "to": {}, + "type": "BATCH_EMAIL", + "updatedAt": "2024-05-29T22:38:43.603Z", + "updatedById": "62365053", + "webversion": {} + } + ], + "paging": { + "next": { + "after": "Mw%3D%3D", + "link": "https://api.hubapi.com/marketing/emails/2026-03?limit=3&sort=-updatedAt&after=Mw%3D%3D" + } + } +} diff --git a/providers/hubspot/test/read/marketing-emails/2-last-page.json b/providers/hubspot/test/read/marketing-emails/2-last-page.json new file mode 100644 index 000000000..078ada8a8 --- /dev/null +++ b/providers/hubspot/test/read/marketing-emails/2-last-page.json @@ -0,0 +1,41 @@ +{ + "total": 3, + "results": [ + { + "activeDomain": "44623425.hs-sites.com", + "allEmailCampaignIds": [ + "285771108" + ], + "archived": false, + "businessUnitId": "0", + "content": {}, + "createdAt": "2023-12-08T17:47:58.334Z", + "createdById": "100", + "emailCampaignGroupId": "285768335", + "emailTemplateMode": "DRAG_AND_DROP", + "from": {}, + "id": "149139108889", + "isAb": false, + "isPublished": true, + "isTransactional": true, + "jitterSendTime": true, + "language": "en", + "name": "Ticket received", + "previewKey": "UMDqTsdm", + "primaryEmailCampaignId": "285771108", + "publishDate": "2023-12-08T17:47:59Z", + "publishedAt": "2023-12-08T17:47:58.956Z", + "publishedById": "100", + "publishedByName": "HubSpot System", + "state": "AUTOMATED", + "subcategory": "ticket_opened_kickback_email", + "subject": "Your ticket '{{ticket.subject}}' has been received", + "subscriptionDetails": {}, + "to": {}, + "type": "TICKET_EMAIL", + "updatedAt": "2023-12-08T17:47:59.150Z", + "updatedById": "100", + "webversion": {} + } + ] +} diff --git a/test/hubspot/metadata/read/marketing-emails/main.go b/test/hubspot/metadata/read/marketing-emails/main.go new file mode 100644 index 000000000..478eef2bc --- /dev/null +++ b/test/hubspot/metadata/read/marketing-emails/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + connTest "github.com/amp-labs/connectors/test/hubspot" + "github.com/amp-labs/connectors/test/utils" +) + +func main() { + // Handle Ctrl-C gracefully. + ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer done() + + conn := connTest.GetHubspotConnector(ctx) + + metadata, err := conn.ListObjectMetadata(ctx, []string{ + "marketing/emails", + }) + if err != nil { + utils.Fail("error listing metadata", "error", err) + } + + fmt.Println("Metadata...") + utils.DumpJSON(metadata, os.Stdout) +} diff --git a/test/hubspot/read/marketing-emails/main.go b/test/hubspot/read/marketing-emails/main.go new file mode 100644 index 000000000..d5272dd82 --- /dev/null +++ b/test/hubspot/read/marketing-emails/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/amp-labs/connectors" + "github.com/amp-labs/connectors/common" + connTest "github.com/amp-labs/connectors/test/hubspot" + "github.com/amp-labs/connectors/test/utils" +) + +func main() { + // Handle Ctrl-C gracefully. + ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer done() + + conn := connTest.GetHubspotConnector(ctx) + + res, err := conn.Read(ctx, common.ReadParams{ + ObjectName: "marketing/emails", + Fields: connectors.Fields("subject", "type", "updatedAt"), + // Since: time.Date(2026, 5, 7, 22, 0, 0, 0, time.UTC), + }) + + if err != nil { + utils.Fail("error reading from connector", "error", err) + } + + fmt.Println("Reading...") + utils.DumpJSON(res, os.Stdout) +} diff --git a/test/utils/mockutils/readResult.go b/test/utils/mockutils/readResult.go index 0582c3e36..0fdafa60f 100644 --- a/test/utils/mockutils/readResult.go +++ b/test/utils/mockutils/readResult.go @@ -110,9 +110,16 @@ func (readResultComparator) SubsetAssociationsRaw(actual, expected *common.ReadR // Identifiers checks that actual rows have identifiers matching with expected. // NOTE: Empty strings signify nothing should be compared. -func (c readResultComparator) Identifiers(actual *common.ReadResult, expected *common.ReadResult) *testutils.CompareResult { +func (c readResultComparator) Identifiers(actual *common.ReadResult, + expected *common.ReadResult, +) *testutils.CompareResult { result := testutils.NewCompareResult() for index, datum := range expected.Data { + if index >= len(actual.Data) { + result.AddDiff("Data[%v] does not exist, cannot check Data[%v].Id", index, index) + break + } + expectedID := datum.Id if expectedID != "" { actualID := actual.Data[index].Id diff --git a/test/utils/testroutines/comparator.go b/test/utils/testroutines/comparator.go index ab1d9d5e2..1d17434e2 100644 --- a/test/utils/testroutines/comparator.go +++ b/test/utils/testroutines/comparator.go @@ -54,11 +54,11 @@ func ComparatorPagination( expectedNextPage := resolveTestServerURL(expected.NextPage.String(), serverURL) if !compareNextPageToken(actual.NextPage.String(), expectedNextPage) { - result.Assert(fmt.Sprintf("NextPage mismatch"), expectedNextPage, actual.NextPage.String()) + result.Assert("NextPage", expectedNextPage, actual.NextPage.String()) } - result.Assert(fmt.Sprintf("Rows mismatch"), expected.Rows, actual.Rows) - result.Assert(fmt.Sprintf("Done mismatch"), expected.Done, actual.Done) + result.Assert("Rows", expected.Rows, actual.Rows) + result.Assert("Done", expected.Done, actual.Done) return result } @@ -101,8 +101,8 @@ func compareNextPageToken(actual, expected string) bool { // needs verification rather than a full equality check. func ComparatorSubsetWrite(_ string, actual, expected *common.WriteResult) *testutils.CompareResult { result := testutils.NewCompareResult() - result.Assert(fmt.Sprintf("Success mismatch"), expected.Success, actual.Success) - result.Assert(fmt.Sprintf("RecordId mismatch"), expected.RecordId, actual.RecordId) + result.Assert("Success", expected.Success, actual.Success) + result.Assert("RecordId", expected.RecordId, actual.RecordId) result.Merge(mockutils.WriteResultComparator.SubsetData(actual, expected)) result.Merge(mockutils.ErrorNormalizedComparator.EachErrorEquals(actual.Errors, expected.Errors)) @@ -121,9 +121,9 @@ func ComparatorSubsetWrite(_ string, actual, expected *common.WriteResult) *test // without enforcing strict structural equality across the entire batch. func ComparatorSubsetBatchWrite(_ string, actual, expected *common.BatchWriteResult) *testutils.CompareResult { result := testutils.NewCompareResult() - result.Assert(fmt.Sprintf("Status mismatch"), expected.Status, actual.Status) - result.Assert(fmt.Sprintf("SuccessCount mismatch"), expected.SuccessCount, actual.SuccessCount) - result.Assert(fmt.Sprintf("FailureCount mismatch"), expected.FailureCount, actual.FailureCount) + result.Assert("Status", expected.Status, actual.Status) + result.Assert("SuccessCount", expected.SuccessCount, actual.SuccessCount) + result.Assert("FailureCount", expected.FailureCount, actual.FailureCount) result.Merge(mockutils.BatchWriteResultComparator.SubsetWriteResults(actual, expected)) result.Merge(mockutils.ErrorNormalizedComparator.EachErrorEquals(actual.Errors, expected.Errors)) @@ -197,16 +197,16 @@ func ComparatorSubsetUpsertMetadata(_ string, actual, expected *common.UpsertMet } // Field properties should be the same. This is a hard comparison. - result.Assert(fmt.Sprintf("Fields[%s][%s].FieldName mismatch", propertyName, fieldName), + result.Assert(fmt.Sprintf("Fields[%s][%s].FieldName", propertyName, fieldName), expectedField.FieldName, actualField.FieldName) - result.Assert(fmt.Sprintf("Fields[%s][%s].Action mismatch", propertyName, fieldName), + result.Assert(fmt.Sprintf("Fields[%s][%s].Action", propertyName, fieldName), expectedField.Action, actualField.Action) - result.Assert(fmt.Sprintf("Fields[%s][%s].Warnings mismatch", propertyName, fieldName), + result.Assert(fmt.Sprintf("Fields[%s][%s].Warnings", propertyName, fieldName), expectedField.Warnings, actualField.Warnings) // A set of expected fields must be present if !mapIsSubsetMap(expectedField.Metadata, actualField.Metadata) { - result.Assert(fmt.Sprintf("Fields[%s][%s].Metadata mismatch", propertyName, fieldName), + result.Assert(fmt.Sprintf("Fields[%s][%s].Metadata", propertyName, fieldName), expectedField.Metadata, actualField.Metadata) } }