Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
38 changes: 34 additions & 4 deletions internal/provider/tracking_plan_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
}

Expand Down
240 changes: 240 additions & 0 deletions internal/provider/tracking_plan_resource_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package provider

import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
Expand Down Expand Up @@ -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"),
},
},
})
}
Loading