Skip to content

Commit b1ee12e

Browse files
committed
Add foundational feature matrix system for generation support
- Implement IsFeatureSupported() helper function for Cedar/Fir generation differences - Add feature matrix tracking space capabilities across generations - Include comprehensive test coverage with 14 test cases - Cedar generation: supports all space features including shield spaces - Fir generation: supports private spaces only, shield spaces unsupported - Foundation for graceful handling of generation-specific feature differences
1 parent b8fb6ac commit b1ee12e

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang 1.24.5
2+
terraform 1.5.7
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package heroku
2+
3+
// Feature matrix system for graceful handling of generation differences
4+
// between Cedar and Fir generations in Terraform Provider Heroku.
5+
6+
// featureMatrix defines which features are supported for each generation and resource type.
7+
// This is the single source of truth for feature availability based on the
8+
// unsupported features data from Platform API's 3.sdk Generation endpoints.
9+
var featureMatrix = map[string]map[string]map[string]bool{
10+
"cedar": {
11+
"space": {
12+
"private": true, // All spaces are private
13+
"shield": true, // Cedar supports shield spaces
14+
"trusted_ip_ranges": true,
15+
"private_vpn": true,
16+
"outbound_rules": true,
17+
"private_space_logging": true,
18+
},
19+
},
20+
"fir": {
21+
"space": {
22+
"private": true, // All spaces are private
23+
"shield": false, // Fir does not support shield spaces
24+
"trusted_ip_ranges": false, // trusted_ip_ranges
25+
"private_vpn": false, // private_vpn
26+
"outbound_rules": false, // outbound_rules
27+
"private_space_logging": false, // private_space_logging
28+
},
29+
},
30+
}
31+
32+
// IsFeatureSupported checks if a feature is supported for a given generation and resource type.
33+
// Returns true if the feature is supported, false otherwise.
34+
//
35+
// Parameters:
36+
// - generation: "cedar" or "fir"
37+
// - resourceType: "space", "app", "build", etc.
38+
// - feature: "shield", "trusted_ip_ranges", "private_vpn", etc.
39+
//
40+
// Example:
41+
//
42+
// if IsFeatureSupported("fir", "space", "shield") {
43+
// // proceed with shield configuration
44+
// }
45+
func IsFeatureSupported(generation, resourceType, feature string) bool {
46+
if gen, exists := featureMatrix[generation]; exists {
47+
if res, exists := gen[resourceType]; exists {
48+
if supported, exists := res[feature]; exists {
49+
return supported
50+
}
51+
}
52+
}
53+
54+
// Default to false for any unknown generation/resource/feature combination
55+
return false
56+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package heroku
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestIsFeatureSupported(t *testing.T) {
8+
testCases := []struct {
9+
name string
10+
generation string
11+
resourceType string
12+
feature string
13+
expected bool
14+
}{
15+
// Cedar generation tests - all space features supported
16+
{
17+
name: "Cedar space private should be supported",
18+
generation: "cedar",
19+
resourceType: "space",
20+
feature: "private",
21+
expected: true,
22+
},
23+
{
24+
name: "Cedar space shield should be supported",
25+
generation: "cedar",
26+
resourceType: "space",
27+
feature: "shield",
28+
expected: true,
29+
},
30+
{
31+
name: "Cedar space trusted_ip_ranges should be supported",
32+
generation: "cedar",
33+
resourceType: "space",
34+
feature: "trusted_ip_ranges",
35+
expected: true,
36+
},
37+
38+
// Fir generation tests - private supported, shield and others unsupported
39+
{
40+
name: "Fir space private should be supported",
41+
generation: "fir",
42+
resourceType: "space",
43+
feature: "private",
44+
expected: true,
45+
},
46+
{
47+
name: "Fir space shield should be unsupported",
48+
generation: "fir",
49+
resourceType: "space",
50+
feature: "shield",
51+
expected: false,
52+
},
53+
{
54+
name: "Fir space trusted_ip_ranges should be unsupported",
55+
generation: "fir",
56+
resourceType: "space",
57+
feature: "trusted_ip_ranges",
58+
expected: false,
59+
},
60+
61+
// Unsupported feature tests (features not in matrix)
62+
{
63+
name: "Cedar space unknown_feature should be unsupported (not in matrix)",
64+
generation: "cedar",
65+
resourceType: "space",
66+
feature: "unknown_feature",
67+
expected: false,
68+
},
69+
{
70+
name: "Fir space unknown_feature should be unsupported (not in matrix)",
71+
generation: "fir",
72+
resourceType: "space",
73+
feature: "unknown_feature",
74+
expected: false,
75+
},
76+
77+
// Unknown resource type tests
78+
{
79+
name: "Cedar app features should be unsupported (not implemented yet)",
80+
generation: "cedar",
81+
resourceType: "app",
82+
feature: "some_feature",
83+
expected: false,
84+
},
85+
{
86+
name: "Fir build features should be unsupported (not implemented yet)",
87+
generation: "fir",
88+
resourceType: "build",
89+
feature: "some_feature",
90+
expected: false,
91+
},
92+
93+
// Invalid generation tests
94+
{
95+
name: "Invalid generation should be unsupported",
96+
generation: "invalid",
97+
resourceType: "space",
98+
feature: "shield",
99+
expected: false,
100+
},
101+
{
102+
name: "Empty generation should be unsupported",
103+
generation: "",
104+
resourceType: "space",
105+
feature: "shield",
106+
expected: false,
107+
},
108+
109+
// Edge case tests
110+
{
111+
name: "Empty resource type should be unsupported",
112+
generation: "cedar",
113+
resourceType: "",
114+
feature: "shield",
115+
expected: false,
116+
},
117+
{
118+
name: "Empty feature should be unsupported",
119+
generation: "cedar",
120+
resourceType: "space",
121+
feature: "",
122+
expected: false,
123+
},
124+
}
125+
126+
for _, tc := range testCases {
127+
t.Run(tc.name, func(t *testing.T) {
128+
result := IsFeatureSupported(tc.generation, tc.resourceType, tc.feature)
129+
if result != tc.expected {
130+
t.Errorf("IsFeatureSupported(%q, %q, %q) = %v, expected %v",
131+
tc.generation, tc.resourceType, tc.feature, result, tc.expected)
132+
}
133+
})
134+
}
135+
}
136+
137+
// TestFeatureMatrixConsistency ensures the feature matrix is internally consistent
138+
func TestFeatureMatrixConsistency(t *testing.T) {
139+
// Verify that all generations have at least one resource
140+
for generation, resources := range featureMatrix {
141+
if len(resources) == 0 {
142+
t.Errorf("Generation %s has no resources defined", generation)
143+
}
144+
145+
// Verify that all resources have at least one feature
146+
for resourceType, features := range resources {
147+
if len(features) == 0 {
148+
t.Errorf("Generation %s, resource %s has no features defined", generation, resourceType)
149+
}
150+
151+
// Verify that all features are properly set (no nil values)
152+
for feature, supported := range features {
153+
if feature == "" {
154+
t.Errorf("Generation %s, resource %s has empty feature name", generation, resourceType)
155+
}
156+
// supported is bool, so just verify it's not accidentally unset in a way that would matter
157+
_ = supported // This is intentional - we're just ensuring the value exists
158+
}
159+
}
160+
}
161+
162+
// Verify minimum required features exist for Task 1
163+
// Both generations support private spaces
164+
if !IsFeatureSupported("cedar", "space", "private") {
165+
t.Error("Cedar space private must be supported")
166+
}
167+
if !IsFeatureSupported("fir", "space", "private") {
168+
t.Error("Fir space private must be supported")
169+
}
170+
171+
// Only cedar supports shield spaces
172+
if !IsFeatureSupported("cedar", "space", "shield") {
173+
t.Error("Cedar space shield must be supported")
174+
}
175+
if IsFeatureSupported("fir", "space", "shield") {
176+
t.Error("Fir space shield must be unsupported")
177+
}
178+
}

0 commit comments

Comments
 (0)