Skip to content

Commit f7170dd

Browse files
pieternhectorcast-db
authored andcommitted
Run tests to verify backend tag validation behavior (#814)
## Changes Validation rules on tags are different per cloud (they are passed through to the underlying clusters and as such must comply with cloud-specific validation rules). This change adds tests to confirm the current behavior to ensure the normalization we can apply is in line with how the backend behaves. ## Tests The new integration tests pass (tested locally).
1 parent 7a5a1f4 commit f7170dd

File tree

4 files changed

+335
-0
lines changed

4 files changed

+335
-0
lines changed

internal/tags_test.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
8+
"github.com/databricks/cli/internal/testutil"
9+
"github.com/databricks/databricks-sdk-go"
10+
"github.com/databricks/databricks-sdk-go/service/compute"
11+
"github.com/databricks/databricks-sdk-go/service/jobs"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func testTags(t *testing.T, tags map[string]string) error {
16+
var nodeTypeId string
17+
switch testutil.GetCloud(t) {
18+
case testutil.AWS:
19+
nodeTypeId = "i3.xlarge"
20+
case testutil.Azure:
21+
nodeTypeId = "Standard_DS4_v2"
22+
case testutil.GCP:
23+
nodeTypeId = "n1-standard-4"
24+
}
25+
26+
w, err := databricks.NewWorkspaceClient()
27+
require.NoError(t, err)
28+
29+
ctx := context.Background()
30+
resp, err := w.Jobs.Create(ctx, jobs.CreateJob{
31+
Name: RandomName("test-tags-"),
32+
Tasks: []jobs.Task{
33+
{
34+
TaskKey: "test",
35+
NewCluster: &compute.ClusterSpec{
36+
SparkVersion: "13.3.x-scala2.12",
37+
NumWorkers: 1,
38+
NodeTypeId: nodeTypeId,
39+
},
40+
SparkPythonTask: &jobs.SparkPythonTask{
41+
PythonFile: "/doesnt_exist.py",
42+
},
43+
},
44+
},
45+
Tags: tags,
46+
})
47+
48+
if resp != nil {
49+
t.Cleanup(func() {
50+
w.Jobs.DeleteByJobId(ctx, resp.JobId)
51+
})
52+
}
53+
54+
return err
55+
}
56+
57+
func testTagKey(t *testing.T, key string) error {
58+
return testTags(t, map[string]string{
59+
key: "value",
60+
})
61+
}
62+
63+
func testTagValue(t *testing.T, value string) error {
64+
return testTags(t, map[string]string{
65+
"key": value,
66+
})
67+
}
68+
69+
type tagTestCase struct {
70+
name string
71+
value string
72+
fn func(t *testing.T, value string) error
73+
err string
74+
}
75+
76+
func runTagTestCases(t *testing.T, cases []tagTestCase) {
77+
for i := range cases {
78+
tc := cases[i]
79+
t.Run(tc.name, func(t *testing.T) {
80+
t.Parallel()
81+
err := tc.fn(t, tc.value)
82+
if tc.err == "" {
83+
require.NoError(t, err)
84+
} else {
85+
require.Error(t, err)
86+
msg := strings.ReplaceAll(err.Error(), "\n", " ")
87+
require.Contains(t, msg, tc.err)
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestAccTagKeyAWS(t *testing.T) {
94+
testutil.Require(t, testutil.AWS)
95+
t.Parallel()
96+
97+
runTagTestCases(t, []tagTestCase{
98+
{
99+
name: "invalid",
100+
value: "café",
101+
fn: testTagKey,
102+
err: ` The key must match the regular expression ^[\d \w\+\-=\.:\/@]*$.`,
103+
},
104+
{
105+
name: "unicode",
106+
value: "🍎",
107+
fn: testTagKey,
108+
err: ` contains non-latin1 characters.`,
109+
},
110+
{
111+
name: "empty",
112+
value: "",
113+
fn: testTagKey,
114+
err: ` the minimal length is 1, and the maximum length is 127.`,
115+
},
116+
{
117+
name: "valid",
118+
value: "cafe",
119+
fn: testTagKey,
120+
err: ``,
121+
},
122+
})
123+
}
124+
125+
func TestAccTagValueAWS(t *testing.T) {
126+
testutil.Require(t, testutil.AWS)
127+
t.Parallel()
128+
129+
runTagTestCases(t, []tagTestCase{
130+
{
131+
name: "invalid",
132+
value: "café",
133+
fn: testTagValue,
134+
err: ` The value must match the regular expression ^[\d \w\+\-=\.:/@]*$.`,
135+
},
136+
{
137+
name: "unicode",
138+
value: "🍎",
139+
fn: testTagValue,
140+
err: ` contains non-latin1 characters.`,
141+
},
142+
{
143+
name: "valid",
144+
value: "cafe",
145+
fn: testTagValue,
146+
err: ``,
147+
},
148+
})
149+
}
150+
151+
func TestAccTagKeyAzure(t *testing.T) {
152+
testutil.Require(t, testutil.Azure)
153+
t.Parallel()
154+
155+
runTagTestCases(t, []tagTestCase{
156+
{
157+
name: "invalid",
158+
value: "café?",
159+
fn: testTagKey,
160+
err: ` The key must match the regular expression ^[^<>\*&%;\\\/\+\?]*$.`,
161+
},
162+
{
163+
name: "unicode",
164+
value: "🍎",
165+
fn: testTagKey,
166+
err: ` contains non-latin1 characters.`,
167+
},
168+
{
169+
name: "empty",
170+
value: "",
171+
fn: testTagKey,
172+
err: ` the minimal length is 1, and the maximum length is 512.`,
173+
},
174+
{
175+
name: "valid",
176+
value: "cafe",
177+
fn: testTagKey,
178+
err: ``,
179+
},
180+
})
181+
}
182+
183+
func TestAccTagValueAzure(t *testing.T) {
184+
testutil.Require(t, testutil.Azure)
185+
t.Parallel()
186+
187+
runTagTestCases(t, []tagTestCase{
188+
{
189+
name: "unicode",
190+
value: "🍎",
191+
fn: testTagValue,
192+
err: ` contains non-latin1 characters.`,
193+
},
194+
{
195+
name: "valid",
196+
value: "cafe",
197+
fn: testTagValue,
198+
err: ``,
199+
},
200+
})
201+
}
202+
203+
func TestAccTagKeyGCP(t *testing.T) {
204+
testutil.Require(t, testutil.GCP)
205+
t.Parallel()
206+
207+
runTagTestCases(t, []tagTestCase{
208+
{
209+
name: "invalid",
210+
value: "café?",
211+
fn: testTagKey,
212+
err: ` The key must match the regular expression ^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$.`,
213+
},
214+
{
215+
name: "unicode",
216+
value: "🍎",
217+
fn: testTagKey,
218+
err: ` contains non-latin1 characters.`,
219+
},
220+
{
221+
name: "empty",
222+
value: "",
223+
fn: testTagKey,
224+
err: ` the minimal length is 1, and the maximum length is 63.`,
225+
},
226+
{
227+
name: "valid",
228+
value: "cafe",
229+
fn: testTagKey,
230+
err: ``,
231+
},
232+
})
233+
}
234+
235+
func TestAccTagValueGCP(t *testing.T) {
236+
testutil.Require(t, testutil.GCP)
237+
t.Parallel()
238+
239+
runTagTestCases(t, []tagTestCase{
240+
{
241+
name: "invalid",
242+
value: "café",
243+
fn: testTagValue,
244+
err: ` The value must match the regular expression ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$.`,
245+
},
246+
{
247+
name: "unicode",
248+
value: "🍎",
249+
fn: testTagValue,
250+
err: ` contains non-latin1 characters.`,
251+
},
252+
{
253+
name: "valid",
254+
value: "cafe",
255+
fn: testTagValue,
256+
err: ``,
257+
},
258+
})
259+
}

internal/testutil/cloud.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package testutil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
type Cloud int
8+
9+
const (
10+
AWS Cloud = iota
11+
Azure
12+
GCP
13+
)
14+
15+
// Implement [Requirement].
16+
func (c Cloud) Verify(t *testing.T) {
17+
if c != GetCloud(t) {
18+
t.Skipf("Skipping %s-specific test", c)
19+
}
20+
}
21+
22+
func (c Cloud) String() string {
23+
switch c {
24+
case AWS:
25+
return "AWS"
26+
case Azure:
27+
return "Azure"
28+
case GCP:
29+
return "GCP"
30+
default:
31+
return "unknown"
32+
}
33+
}
34+
35+
func GetCloud(t *testing.T) Cloud {
36+
env := GetEnvOrSkipTest(t, "CLOUD_ENV")
37+
switch env {
38+
case "aws":
39+
return AWS
40+
case "azure":
41+
return Azure
42+
case "gcp":
43+
return GCP
44+
default:
45+
t.Fatalf("Unknown cloud environment: %s", env)
46+
}
47+
return -1
48+
}

internal/testutil/env.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ func CleanupEnvironment(t *testing.T) {
3535
t.Setenv("USERPROFILE", pwd)
3636
}
3737
}
38+
39+
// GetEnvOrSkipTest proceeds with test only with that env variable
40+
func GetEnvOrSkipTest(t *testing.T, name string) string {
41+
value := os.Getenv(name)
42+
if value == "" {
43+
t.Skipf("Environment variable %s is missing", name)
44+
}
45+
return value
46+
}

internal/testutil/requirement.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package testutil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// Requirement is the interface for test requirements.
8+
type Requirement interface {
9+
Verify(t *testing.T)
10+
}
11+
12+
// Require should be called at the beginning of a test to ensure that all
13+
// requirements are met before running the test.
14+
// If any requirement is not met, the test will be skipped.
15+
func Require(t *testing.T, requirements ...Requirement) {
16+
for _, r := range requirements {
17+
r.Verify(t)
18+
}
19+
}

0 commit comments

Comments
 (0)