diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4e09b..0db6329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.5.0 (August 11, 2025) +Bump the allowed number of rules in a Tracking Plan to 2000 items depending on workspace limits. + ## 1.4.1 (July 21, 2025) Bump the Go language version to `v1.24.2` to fix the following security vulnerabilities: diff --git a/internal/provider/tracking_plan_resource.go b/internal/provider/tracking_plan_resource.go index 003f58b..c58279b 100644 --- a/internal/provider/tracking_plan_resource.go +++ b/internal/provider/tracking_plan_resource.go @@ -34,6 +34,8 @@ type trackingPlanResource struct { authContext context.Context } +var MaxRules = 2000 + func (r *trackingPlanResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_tracking_plan" } @@ -87,7 +89,7 @@ To see an exact representation of this Tracking Plan's rules, please use the dat This field is currently limited to 200 items.`, Validators: []validator.Set{ - setvalidator.SizeAtMost(MaxPageSize), + setvalidator.SizeAtMost(MaxRules), }, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ @@ -260,6 +262,8 @@ func (r *trackingPlanResource) Read(ctx context.Context, req resource.ReadReques } state.Rules = rules } else { + outRules := []api.RuleV1{} + out, body, err := r.client.TrackingPlansAPI.ListRulesFromTrackingPlan(r.authContext, id).Pagination(*api.NewPaginationInput(MaxPageSize)).Execute() if body != nil { defer body.Body.Close() @@ -273,15 +277,41 @@ func (r *trackingPlanResource) Read(ctx context.Context, req resource.ReadReques return } - outRules := out.Data.GetRules() + outRules = append(outRules, out.Data.GetRules()...) + nextPointer := out.Data.GetPagination().Next.Get() + + for nextPointer != nil && len(outRules) < MaxRules { + paginationInput := *api.NewPaginationInput(MaxPageSize) + paginationInput.SetCursor(*nextPointer) + + out, body, err = r.client.TrackingPlansAPI.ListRulesFromTrackingPlan(r.authContext, id).Pagination(paginationInput).Execute() + if body != nil { + defer body.Body.Close() + } + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to read Tracking Plan rules (ID: %s)", id), + getError(err, body), + ) + + return + } + + outRules = append(outRules, out.Data.GetRules()...) + nextPointer = out.Data.GetPagination().Next.Get() + } + + // Limit the number of rules to MAX_RULES + if len(outRules) > MaxRules { + outRules = outRules[:MaxRules] + } + err = state.Fill(trackingPlan, &outRules) if err != nil { resp.Diagnostics.AddError( "Unable to populate Tracking Plan state", err.Error(), ) - - return } } diff --git a/internal/provider/tracking_plan_resource_test.go b/internal/provider/tracking_plan_resource_test.go index 43a52f2..642b149 100644 --- a/internal/provider/tracking_plan_resource_test.go +++ b/internal/provider/tracking_plan_resource_test.go @@ -1,8 +1,11 @@ package provider import ( + "fmt" "net/http" "net/http/httptest" + "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -240,3 +243,240 @@ func TestAccTrackingPlanResource(t *testing.T) { }, }) } + +func TestAccTrackingPlanResource_ErrorHandling(t *testing.T) { + t.Parallel() + + t.Run("handles invalid JSON schema", func(t *testing.T) { + t.Parallel() + + fakeServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("content-type", "application/json") + if req.URL.Path == "/tracking-plans" { + _, _ = w.Write([]byte(` + { + "data": { + "trackingPlan": { + "id": "test-id", + "name": "Test Plan", + "type": "LIVE" + } + } + }`)) + } else if req.Method == http.MethodDelete { + _, _ = w.Write([]byte(`{"data": {}}`)) + } else { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error": "Invalid JSON schema"}`)) + } + }), + ) + defer fakeServer.Close() + + providerConfig := ` + provider "segment" { + url = "` + fakeServer.URL + `" + token = "abc123" + } + ` + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "segment_tracking_plan" "test" { + name = "Test Tracking Plan" + type = "LIVE" + rules = [ + { + key = "Test Rule" + type = "TRACK" + version = 1 + json_schema = jsonencode({"invalid": "schema"}) + } + ] + } + `, + ExpectError: regexp.MustCompile("Unable to create Tracking Plan rules"), + }, + }, + }) + }) +} + +func TestAccTrackingPlanResource_PaginationHandling(t *testing.T) { + t.Parallel() + + ruleRequestCount := 0 + fakeServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("content-type", "application/json") + + if req.URL.Path == "/tracking-plans" { + _, _ = w.Write([]byte(` + { + "data": { + "trackingPlan": { + "id": "test-tracking-plan-id", + "name": "Test Tracking Plan", + "type": "LIVE", + "createdAt": "2021-11-16T00:06:19.000Z", + "updatedAt": "2021-11-16T00:06:19.000Z" + } + } + }`)) + } else if req.URL.Path == "/tracking-plans/test-tracking-plan-id" { + _, _ = w.Write([]byte(` + { + "data": { + "trackingPlan": { + "id": "test-tracking-plan-id", + "name": "Test Tracking Plan", + "type": "LIVE", + "createdAt": "2021-11-16T00:06:19.000Z", + "updatedAt": "2021-11-16T00:06:19.000Z" + } + } + }`)) + } else if req.URL.Path == "/tracking-plans/test-tracking-plan-id/rules" { + ruleRequestCount++ + + // Simulate pagination - first page returns cursor, second page returns empty + if ruleRequestCount == 1 { + _, _ = w.Write([]byte(` + { + "data": { + "rules": [ + { + "key": "Rule 1", + "type": "TRACK", + "version": 1, + "jsonSchema": {"properties": {}}, + "createdAt": "2023-09-08T19:02:55.000Z", + "updatedAt": "2023-09-08T19:02:55.000Z" + } + ], + "pagination": { + "current": "MA==", + "next": "Mg==", + "totalEntries": 2 + } + } + }`)) + } else { + _, _ = w.Write([]byte(` + { + "data": { + "rules": [ + { + "key": "Rule 2", + "type": "IDENTIFY", + "version": 1, + "jsonSchema": {"properties": {}}, + "createdAt": "2023-09-08T19:02:55.000Z", + "updatedAt": "2023-09-08T19:02:55.000Z" + } + ], + "pagination": { + "current": "Mg==", + "totalEntries": 2 + } + } + }`)) + } + + if req.Method == http.MethodPost { + // Handle rule replacement + _, _ = w.Write([]byte(`{"data": {}}`)) + } + } else { + _, _ = w.Write([]byte(`{"data": {}}`)) + } + }), + ) + defer fakeServer.Close() + + providerConfig := ` + provider "segment" { + url = "` + fakeServer.URL + `" + token = "abc123" + } + ` + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "segment_tracking_plan" "test" { + name = "Test Tracking Plan" + type = "LIVE" + rules = [ + { + key = "Test Rule" + type = "TRACK" + version = 1 + json_schema = jsonencode({}) + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("segment_tracking_plan.test", "id", "test-tracking-plan-id"), + resource.TestCheckResourceAttr("segment_tracking_plan.test", "name", "Test Tracking Plan"), + ), + }, + }, + }) +} + +func TestAccTrackingPlanResource_MaxRulesValidation(t *testing.T) { + t.Parallel() + + fakeServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") + _, _ = w.Write([]byte(`{"data": {}}`)) + }), + ) + defer fakeServer.Close() + + providerConfig := ` + provider "segment" { + url = "` + fakeServer.URL + `" + token = "abc123" + } + ` + + // Generate more than MAX_RULES (2000) rules to test validation + var rulesConfig strings.Builder + rulesConfig.WriteString("rules = [\n") + for i := 0; i < 2001; i++ { + rulesConfig.WriteString(fmt.Sprintf(` + { + key = "Rule %d" + type = "TRACK" + version = 1 + json_schema = jsonencode({}) + },`, i)) + } + rulesConfig.WriteString("\n]") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "segment_tracking_plan" "test" { + name = "Test Tracking Plan" + type = "LIVE" + ` + rulesConfig.String() + ` + } + `, + ExpectError: regexp.MustCompile("Attribute rules set must contain at most 2000 elements"), + }, + }, + }) +}