Skip to content

Commit 670bc39

Browse files
authored
#52775 - Add Integration Tests for metadata.fields (#1042)
* added new test files * addressing comment by @crobby
1 parent cdc2f71 commit 670bc39

3 files changed

Lines changed: 445 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package tests
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"time"
13+
14+
"github.com/rancher/steve/pkg/server"
15+
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
16+
"github.com/stretchr/testify/assert"
17+
"gopkg.in/yaml.v3"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20+
"k8s.io/apimachinery/pkg/runtime/schema"
21+
)
22+
23+
var (
24+
testdataSchemaRefreshDir = filepath.Join("testdata", "schema_refresh")
25+
)
26+
27+
type SchemaRefreshScenario struct {
28+
Name string `yaml:"name"`
29+
SchemaID string `yaml:"schemaID"`
30+
CRDName string `yaml:"crdName"`
31+
Resource map[string]interface{} `yaml:"resource"`
32+
ExpectedInitial [][]any `yaml:"expectedInitial"`
33+
ExpectedUpdated [][]any `yaml:"expectedUpdated"`
34+
InitialCRD map[string]interface{} `yaml:"initialCRD"`
35+
UpdatedCRD map[string]interface{} `yaml:"updatedCRD"`
36+
}
37+
38+
type SchemaRefreshTestConfig struct {
39+
Scenarios []SchemaRefreshScenario `yaml:"scenarios"`
40+
}
41+
42+
type FieldsResponse struct {
43+
Data []struct {
44+
Metadata struct {
45+
Name string `json:"name"`
46+
Fields []any `json:"fields"`
47+
} `json:"metadata"`
48+
} `json:"data"`
49+
}
50+
51+
func (i *IntegrationSuite) TestSchemaRefresh() {
52+
ctx := i.T().Context()
53+
54+
steveHandler, err := server.New(ctx, i.restCfg, &server.Options{
55+
SQLCache: true,
56+
SQLCacheFactoryOptions: factory.CacheFactoryOptions{
57+
GCInterval: 15 * time.Minute,
58+
GCKeepCount: 1000,
59+
},
60+
})
61+
i.Require().NoError(err)
62+
63+
httpServer := httptest.NewServer(steveHandler)
64+
defer httpServer.Close()
65+
66+
baseURL := httpServer.URL
67+
68+
// Find all scenario files
69+
matches, err := filepath.Glob(filepath.Join(testdataSchemaRefreshDir, "*.test.yaml"))
70+
i.Require().NoError(err)
71+
72+
for _, match := range matches {
73+
scenarioFile := filepath.Base(match)
74+
scenarioFile = strings.TrimSuffix(scenarioFile, ".test.yaml")
75+
76+
// Load test configuration
77+
configFile, err := os.Open(match)
78+
i.Require().NoError(err)
79+
80+
var testConfig SchemaRefreshTestConfig
81+
err = yaml.NewDecoder(configFile).Decode(&testConfig)
82+
configFile.Close()
83+
i.Require().NoError(err)
84+
85+
// Run each scenario in the file
86+
for _, scenario := range testConfig.Scenarios {
87+
i.Run(scenario.Name, func() {
88+
i.testSchemaRefreshScenario(ctx, scenario, baseURL)
89+
})
90+
}
91+
}
92+
}
93+
94+
func (i *IntegrationSuite) testSchemaRefreshScenario(ctx context.Context, scenario SchemaRefreshScenario, baseURL string) {
95+
// 1. Convert initialCRD and updatedCRD maps to unstructured objects
96+
initialCRD := &unstructured.Unstructured{Object: scenario.InitialCRD}
97+
updatedCRD := &unstructured.Unstructured{Object: scenario.UpdatedCRD}
98+
99+
// 2. Define GVRs
100+
crdGVR := schema.GroupVersionResource{
101+
Group: "apiextensions.k8s.io",
102+
Version: "v1",
103+
Resource: "customresourcedefinitions",
104+
}
105+
106+
testGVR := i.parseGVRFromSchemaID(scenario.SchemaID)
107+
108+
// 3. Apply initial CRD
109+
err := i.doApply(ctx, initialCRD, crdGVR)
110+
i.Require().NoError(err)
111+
defer i.doDelete(ctx, initialCRD, crdGVR)
112+
113+
// Wait for schema to be available
114+
i.waitForSchema(baseURL, testGVR)
115+
116+
// 4. Create test resource
117+
resource := &unstructured.Unstructured{Object: scenario.Resource}
118+
err = i.doApply(ctx, resource, testGVR)
119+
i.Require().NoError(err)
120+
defer i.doDelete(ctx, resource, testGVR)
121+
122+
// 5. Verify initial metadata.fields
123+
resourceName := scenario.Resource["metadata"].(map[string]interface{})["name"].(string)
124+
url := fmt.Sprintf("%s/v1/%s?filter=metadata.name=%s&filter=metadata.namespace=default", baseURL, scenario.SchemaID, resourceName)
125+
126+
i.Require().EventuallyWithT(func(c *assert.CollectT) {
127+
fields := i.getResourceFields(c, url)
128+
i.verifyFieldsWithCollectT(c, fields, scenario.ExpectedInitial)
129+
}, 10*time.Second, 500*time.Millisecond, "Initial schema should be available")
130+
131+
// 6. Update CRD
132+
// Must get current CRD for resourceVersion
133+
currentCRD, err := i.client.Resource(crdGVR).Get(ctx, scenario.CRDName, metav1.GetOptions{})
134+
i.Require().NoError(err)
135+
136+
// Preserve resourceVersion when updating
137+
updatedCRD.SetResourceVersion(currentCRD.GetResourceVersion())
138+
139+
_, err = i.client.Resource(crdGVR).Update(ctx, updatedCRD, metav1.UpdateOptions{})
140+
i.Require().NoError(err)
141+
142+
// 7. Wait for schema refresh and verify updated metadata.fields
143+
i.Require().EventuallyWithT(func(c *assert.CollectT) {
144+
fields := i.getResourceFields(c, url)
145+
i.verifyFieldsWithCollectT(c, fields, scenario.ExpectedUpdated)
146+
}, 30*time.Second, 1*time.Second, "Schema should refresh with updated columns")
147+
}
148+
149+
func (i *IntegrationSuite) parseGVRFromSchemaID(schemaID string) schema.GroupVersionResource {
150+
parts := strings.Split(schemaID, ".")
151+
i.Require().GreaterOrEqual(len(parts), 3, "schemaID should have at least 3 parts")
152+
153+
// Last part is plural resource name (e.g., "zerotosomes")
154+
plural := parts[len(parts)-1]
155+
156+
// Everything except last part is the group
157+
group := strings.Join(parts[:len(parts)-1], ".")
158+
159+
return schema.GroupVersionResource{
160+
Group: group,
161+
Version: "v1",
162+
Resource: plural,
163+
}
164+
}
165+
166+
func (i *IntegrationSuite) getResourceFields(t assert.TestingT, url string) []any {
167+
resp, err := http.Get(url)
168+
if !assert.NoError(t, err) {
169+
return nil
170+
}
171+
defer resp.Body.Close()
172+
173+
if !assert.Equal(t, http.StatusOK, resp.StatusCode) {
174+
return nil
175+
}
176+
177+
var parsed FieldsResponse
178+
if err := json.NewDecoder(resp.Body).Decode(&parsed); !assert.NoError(t, err) {
179+
return nil
180+
}
181+
182+
if !assert.NotEmpty(t, parsed.Data) {
183+
return nil
184+
}
185+
186+
return parsed.Data[0].Metadata.Fields
187+
}
188+
189+
func (i *IntegrationSuite) verifyFieldsWithCollectT(c *assert.CollectT, actualFields []any, expectedRows [][]any) {
190+
if !assert.Len(c, expectedRows, 1, "Expected single row") {
191+
return
192+
}
193+
expected := expectedRows[0]
194+
195+
if !assert.Len(c, actualFields, len(expected), "Field count mismatch") {
196+
return
197+
}
198+
199+
for idx, expectedField := range expected {
200+
switch expectedField {
201+
case "$timestamp":
202+
_, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", actualFields[idx]))
203+
assert.NoError(c, err, "Expected timestamp at index %d", idx)
204+
case "$skip":
205+
continue
206+
default:
207+
assert.Equal(c, expectedField, actualFields[idx], "Field mismatch at index %d", idx)
208+
}
209+
}
210+
}

tests/integration/testdata/columns/scenario1.test.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ tests:
1111
- schemaID: timestamps.cattle.io.dates
1212
expected:
1313
- [the-date, $skip, $skip, $timestamp, $skip, $timestamp, $skip, $timestamp]
14+
- schemaID: provisioning.cattle.io.clusters
15+
query: filter=metadata.name=test-cluster&filter=metadata.namespace=default
16+
expected:
17+
- [test-cluster, $skip, $skip, $timestamp, $skip, $skip]
1418
- schemaID: events
1519
query: filter=metadata.name=event-1
1620
expected:
@@ -160,6 +164,14 @@ conditions:
160164
- type: Selected
161165
lastUpdateTime: "2025-01-01T00:00:00Z"
162166
---
167+
apiVersion: provisioning.cattle.io/v1
168+
kind: Cluster
169+
metadata:
170+
name: test-cluster
171+
namespace: default
172+
spec:
173+
kubernetesVersion: v1.28.0
174+
---
163175
apiVersion: v1
164176
kind: ConfigMap
165177
metadata:

0 commit comments

Comments
 (0)