terraform-provider-testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited terraform-provider-testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Comprehensive test-driven development for Terraform providers emphasizing iterative co-development of tests, generators, and schemas. This skill drives the development of production-ready provider code through systematic testing and root cause analysis.
Tests, generators, and schemas are interdependent systems that evolve together. When tests fail, the response is ALWAYS to fix the root cause - never to skip, disable, or work around.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CO-DEVELOPMENT TRIANGLE │
│ │
│ ┌─────────┐ │
│ │ TESTS │ │
│ └────┬────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │GENERATORS│◄────►│ SCHEMAS │◄────►│ RESOURCES│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Tests drive → Generator fixes → Schema corrections → Resource updates │
└─────────────────────────────────────────────────────────────────────────────┘ALWAYS develop and fix unless ONE of these conditions applies:
| Skip Condition | Example | Action |
|---|---|---|
| External cloud credentials required | AWS VPC Site needs AWS_ACCESS_KEY_ID | t.Skip("requires AWS credentials") |
| Premium/enterprise licensing required | Bot defense needs advanced license | t.Skip("requires premium licensing") |
| Third-party service account needed | External OIDC provider | t.Skip("requires external service account") |
NEVER skip for these reasons - fix them instead:
| Invalid Skip Reason | Correct Action |
|---|---|
| "Schema attribute missing" | Fix generator, regenerate |
| "State drift on nested blocks" | Fix generator's Read/Schema handling |
| "Import produces diff" | Fix ImportState function in generator |
| "API returns unexpected format" | Fix client types or resource parsing |
| "Test is too complex" | Break into smaller tests, still implement |
| "Generator doesn't support this" | Enhance generator to support it |
When a test fails, investigate and fix in this order:
1. GENERATOR ISSUE?
│ Symptoms: Same failure across ALL resources of similar type
│ Fix: tools/generate-all-schemas.go → go generate ./...
│
2. SCHEMA ISSUE?
│ Symptoms: Attribute handling, type mismatches, nested block problems
│ Fix: Generator's schema generation logic → regenerate
│
3. RESOURCE IMPLEMENTATION ISSUE?
│ Symptoms: Single resource fails, API-specific parsing
│ Fix: The specific resource's CRUD methods
│
4. TEST ISSUE?
│ Symptoms: Wrong assertions, incorrect config, test logic error
│ Fix: Test code only AFTER ruling out 1-3CRITICAL: All Go source code MUST use tabs for indentation - this is enforced by gofmt.
| File Type | Indentation | Standard |
|---|---|---|
Go source files (.go) | Tabs | gofmt enforced |
| Embedded Terraform/HCL in heredocs | 2 spaces | Terraform convention |
Why tabs for Go:
gofmt is the canonical Go formatter and uses tabsgofmt before commitgofmt output// ✅ CORRECT: Tabs for Go code indentation (gofmt standard)
func TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccExampleConfig_basic(rName),
},
},
})
}Terraform/HCL inside Go heredocs uses 2 spaces (Terraform convention):
// ✅ CORRECT: Tabs for Go, 2 spaces for embedded HCL
func testAccExampleConfig_basic(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
nested_block {
attribute = "value"
}
}
`, rName)
}Key distinction:
func and return fmt.Sprintf lines use tabs (Go code)Always run gofmt before committing:
# Format all Go files in place
gofmt -w .
# Check formatting without modifying (CI mode)
gofmt -d . | grep -q . && echo "Formatting needed" || echo "OK"
# Format with goimports (also organizes imports)
goimports -w .Configure your editor for Go's tab standard:
VS Code settings:
{
"[go]": {
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.formatOnSave": true
}
}Vim settings:
" For Go files - use tabs (gofmt standard)
autocmd FileType go setlocal noexpandtab tabstop=4 shiftwidth=4Every test development follows this iterative pattern:
# Phase 1: Write Initial Test
vim internal/provider/example_resource_test.go
# Phase 2: Run Test (expect failures)
F5XC_API_URL="https://tenant.console.ves.volterra.io" \
F5XC_P12_FILE="/path/to/cert.p12" \
F5XC_P12_PASSWORD="password" \ # pragma: allowlist secret
TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/...
# Phase 3: Analyze Failure - Determine Root Cause
# Ask: Is this a GENERATOR issue affecting all resources?
# Is this a SCHEMA issue with attribute handling?
# Is this a RESOURCE issue with this specific API?
# Is this a TEST issue with my assertions?
# Phase 4: Fix at the Appropriate Level
# If generator: vim tools/generate-all-schemas.go && go generate ./...
# If schema: Fix in generator, regenerate
# If resource: Fix specific resource implementation
# If test: Fix test assertions/config
# Phase 5: Re-run and Verify
TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/...
# REPEAT until test passes - NEVER skipTest Failed
│
├── Error mentions "attribute not found in schema"?
│ └── FIX: Generator schema generation → regenerate ALL resources
│
├── Error shows state drift on nested blocks?
│ └── FIX: Generator's Read method handling of nested structures
│
├── ImportStateVerify shows diff?
│ └── FIX: Generator's ImportState function
│
├── API returns 400/422 with field error?
│ └── FIX: Generator's Create/Update request building
│
├── Computed field not populated after Read?
│ └── FIX: Generator's Read response → state mapping
│
├── Same error pattern across multiple resource types?
│ └── FIX: Generator (systemic issue) → regenerate
│
├── Error only in this specific resource?
│ └── FIX: Resource implementation OR client types
│
└── Assertion doesn't match expected value?
└── INVESTIGATE: Is expected value correct? Is resource behavior correct?
├── Resource behavior wrong → Fix resource
└── Assertion wrong → Fix testExample 1: Nested Block State Drift
TEST FAILURE: inconsistent result after apply
- default_route_pools.0.pool.namespace: "" => "test-ns"
ANALYSIS: Generator's Read method doesn't populate nested blocks correctly
FIX LOCATION: tools/generate-all-schemas.go
FIX TYPE: Update nested block flattening logic
STEPS:
1. Identify pattern in generator for nested block handling
2. Fix the flattening/expansion logic
3. go generate ./...
4. Re-run test
5. Verify ALL similar resources now workExample 2: Missing Schema Attribute
TEST FAILURE: attribute "labels" not found in schema
ANALYSIS: Generator doesn't include labels attribute from OpenAPI spec
FIX LOCATION: tools/generate-all-schemas.go
FIX TYPE: Add labels field to schema generation
STEPS:
1. Check OpenAPI spec confirms labels exists
2. Update generator to include labels in schema
3. go generate ./...
4. Re-run test
5. Verify all resources with labels now have the attributeExample 3: Import State Incomplete
TEST FAILURE: ImportStateVerify found differences:
- description: "test" => ""
ANALYSIS: ImportState doesn't set all attributes from API response
FIX LOCATION: Generator's ImportState template or Read method
FIX TYPE: Ensure Read populates all importable attributes
STEPS:
1. Check API response includes description
2. Fix generator's Read to map description to state
3. go generate ./...
4. Re-run import test
5. Verify import now preserves all attributesfunc TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy,
Steps: []resource.TestStep{
// Step 1: Create and verify
{
Config: testAccExampleConfig_basic(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionCreate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("name"), knownvalue.StringExact(rName)),
},
},
// Step 2: Import and verify state roundtrip
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}Every testable resource MUST have:
| Test Type | Purpose | Skip Only If |
|---|---|---|
_basic | Create, Read, basic attributes | External credentials/licensing |
_update | Modify mutable attributes | External credentials/licensing |
_import | Import existing resource | External credentials/licensing |
_disappears | Handle external deletion | External credentials/licensing |
| Data source test | Verify data source reads resource | External credentials/licensing |
// REQUIRED test structure for each resource
func TestAccExampleResource_basic(t *testing.T) { ... }
func TestAccExampleResource_update(t *testing.T) { ... }
func TestAccExampleResource_disappears(t *testing.T) { ... }
func TestAccExampleDataSource_basic(t *testing.T) { ... }Validate Terraform plan before resources are applied:
{
Config: testAccConfig,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
// Verify expected action
plancheck.ExpectResourceAction("f5xc_namespace.test",
plancheck.ResourceActionCreate),
// Verify known values in plan
plancheck.ExpectKnownValue("f5xc_namespace.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-ns")),
// Verify unknown values (computed)
plancheck.ExpectUnknownValue("f5xc_namespace.test",
tfjsonpath.New("id")),
},
PostApply: []plancheck.PlanCheck{
// After apply, plan should be empty (no drift)
plancheck.ExpectEmptyPlan(),
},
},
}Validate Terraform state after resources are applied:
{
Config: testAccConfig,
ConfigStateChecks: []statecheck.StateCheck{
// Verify exact string value
statecheck.ExpectKnownValue("f5xc_namespace.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-ns")),
// Verify boolean
statecheck.ExpectKnownValue("f5xc_app_firewall.test",
tfjsonpath.New("blocking"),
knownvalue.Bool(true)),
// Verify list size
statecheck.ExpectKnownValue("f5xc_http_lb.test",
tfjsonpath.New("domains"),
knownvalue.ListSizeExact(2)),
// Verify nested object
statecheck.ExpectKnownValue("f5xc_http_lb.test",
tfjsonpath.New("default_route_pools").AtSliceIndex(0),
knownvalue.ObjectPartial(map[string]knownvalue.Check{
"weight": knownvalue.Int64Exact(1),
"priority": knownvalue.Int64Exact(1),
})),
},
}| Check Type | Usage | Example |
|---|---|---|
StringExact | Exact string match | knownvalue.StringExact("value") |
StringRegexp | Regex match | knownvalue.StringRegexp(regexp.MustCompile(^tf-)) |
Bool | Boolean value | knownvalue.Bool(true) |
Int64Exact | Exact integer | knownvalue.Int64Exact(42) |
ListExact | Exact list | knownvalue.ListExact([]knownvalue.Check{...}) |
ListSizeExact | List length | knownvalue.ListSizeExact(3) |
ListPartial | Partial list match | knownvalue.ListPartial(map[int]knownvalue.Check{0: ...}) |
ObjectExact | Exact object | knownvalue.ObjectExact(map[string]knownvalue.Check{...}) |
ObjectPartial | Partial object | knownvalue.ObjectPartial(map[string]knownvalue.Check{...}) |
Null | Null value | knownvalue.Null() |
NotNull | Not null | knownvalue.NotNull() |
// Simple attribute
tfjsonpath.New("name")
// Nested attribute
tfjsonpath.New("metadata").AtMapKey("labels")
// List index
tfjsonpath.New("domains").AtSliceIndex(0)
// Complex nested path
tfjsonpath.New("spec").AtMapKey("routes").AtSliceIndex(0).AtMapKey("match")RULE: Create a custom test namespace unless the API spec EXPLICITLY requires system, default, or shared.
// ✅ CORRECT: Custom namespace
func testAccExampleConfig_basic(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
}
`, rName)
}
// ❌ WRONG: Using system namespace without spec requirement
func testAccExampleConfig_wrong(name string) string {
return fmt.Sprintf(`
resource "f5xc_example" "test" {
name = %[1]q
namespace = "system" // NEVER unless spec requires it
}
`, name)
}Does the OpenAPI spec require a specific namespace?
├── YES: system/default/shared required
│ ├── Document WHY in test comments with spec reference
│ └── Use the required namespace
└── NO: Custom namespace allowed
└── ALWAYS create f5xc_namespace.test and reference itfunc TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy,
Steps: []resource.TestStep{
// Create
{
Config: testAccExampleConfig_basic(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionCreate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("name"), knownvalue.StringExact(rName)),
},
},
// Update
{
Config: testAccExampleConfig_updated(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionUpdate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("description"),
knownvalue.StringExact("updated")),
},
},
// Import
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}func testAccCheckExampleDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*client.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "f5xc_example" {
continue
}
namespace := rs.Primary.Attributes["namespace"]
name := rs.Primary.Attributes["name"]
_, err := client.GetExample(context.Background(), namespace, name)
if err == nil {
return fmt.Errorf("f5xc_example %s/%s still exists", namespace, name)
}
if !client.IsNotFoundError(err) {
return fmt.Errorf("error checking f5xc_example %s/%s: %w",
namespace, name, err)
}
}
return nil
}{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return "", fmt.Errorf("resource not found: %s", resourceName)
}
return fmt.Sprintf("%s/%s",
rs.Primary.Attributes["namespace"],
rs.Primary.Attributes["name"]), nil
},
}func TestAccExampleDataSource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
dataSourceName := "data.f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccExampleDataSourceConfig(rName),
Check: resource.ComposeAggregateTestCheckFunc(
// Compare data source to resource
resource.TestCheckResourceAttrPair(
dataSourceName, "name",
resourceName, "name"),
resource.TestCheckResourceAttrPair(
dataSourceName, "id",
resourceName, "id"),
resource.TestCheckResourceAttrPair(
dataSourceName, "namespace",
resourceName, "namespace"),
),
},
},
})
}
func testAccExampleDataSourceConfig(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
}
data "f5xc_example" "test" {
name = f5xc_example.test.name
namespace = f5xc_example.test.namespace
}
`, rName)
}| Failure | Root Cause | Fix Location | Fix Action |
|---|---|---|---|
attribute not found in schema | Generator schema incomplete | Generator | Add attribute to schema generation |
planned value does not match | Computed attribute not set | Generator Read | Map API response to state |
inconsistent result after apply | Read not populating state | Generator Read | Fix response → state mapping |
import produces diff | ImportState incomplete | Generator ImportState | Ensure all attributes set |
CheckDestroy failed | Delete not working | Resource Delete | Fix delete API call |
namespace not found | Using system namespace | Test config | Use custom namespace |
permission denied | Wrong namespace | Test config | Check namespace permissions |
ExpectResourceAction failed | Wrong action detected | Resource/Test | Check ForceNew attributes |
| State drift on nested blocks | Flattening logic wrong | Generator | Fix nested block handling |
| Boolean always false | Type conversion issue | Generator Schema | Fix bool type handling |
# Step 1: Enable debug logging
TF_LOG=DEBUG TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/... 2>&1 | tee test.log
# Step 2: Find the API request/response
grep -A 20 "HTTP Request" test.log
grep -A 50 "HTTP Response" test.log
# Step 3: Compare API response to state
# Look for fields in response not making it to state
# Look for type mismatches (string vs int, etc.)
# Step 4: Identify fix location
# If API has data but state doesn't → Generator Read method
# If state has data but plan shows change → Generator Schema/Defaults
# If API returns error → Generator Create/Update request building| Category | Examples | Action |
|---|---|---|
| Core resources | namespace, healthcheck, origin_pool | Full test suite |
| Security policies | app_firewall, service_policy, rate_limiter | Full test suite |
| Load balancers | http_loadbalancer, tcp_loadbalancer | Full test suite |
| Network config | virtual_network, network_policy | Full test suite |
| DNS resources | dns_zone, dns_domain | Full test suite |
| Configuration objects | Any policy, rule, or config resource | Full test suite |
| Category | Examples | Skip Reason |
|---|---|---|
| Cloud sites | aws_vpc_site, azure_vnet_site, gcp_vpc_site | Requires cloud credentials |
| Cloud integrations | cloud_credentials (AWS/Azure/GCP type) | Requires cloud credentials |
| Premium features | bot_defense, advanced_ | Requires premium licensing |
| Third-party | External OIDC, external integrations | Requires external accounts |
// Proper skip documentation
func TestAccAWSVPCSite_basic(t *testing.T) {
t.Skip("Skipping: requires AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)")
}
func TestAccBotDefenseAdvanced_basic(t *testing.T) {
t.Skip("Skipping: requires F5 XC premium/enterprise licensing")
}# Required for ALL acceptance tests
export F5XC_P12_FILE="/path/to/api-certificate.p12"
export F5XC_P12_PASSWORD="your-password" # pragma: allowlist secret
export F5XC_API_URL="https://tenant.console.ves.volterra.io"
export TF_ACC=1# Single test
go test -v -timeout 15m -run=TestAccNamespaceResource_basic ./internal/provider/...
# All tests for a resource
go test -v -timeout 30m -run=TestAccNamespace ./internal/provider/...
# With debug logging
TF_LOG=DEBUG go test -v -timeout 15m -run=TestAccNamespaceResource_basic ./internal/provider/...
# With parallel limit
go test -v -timeout 30m -parallel=4 ./internal/provider/...
# Multiple specific tests
go test -v -timeout 30m -run="TestAccNamespaceResource_basic|TestAccHealthcheckResource_basic" ./internal/provider/...t.Parallel()ImportStateVerifyIgnore without documented reason~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.