package template import ( "context" "fmt" "testing" "text/template" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func testConfig(t *testing.T) *config { c, err := newConfig(context.Background(), "./testdata/config-test-schema/test-schema.json") require.NoError(t, err) return c } func TestTemplateConfigAssignValuesFromFile(t *testing.T) { c := testConfig(t) err := c.assignValuesFromFile("./testdata/config-assign-from-file/config.json") assert.NoError(t, err) assert.Equal(t, int64(1), c.values["int_val"]) assert.Equal(t, float64(2), c.values["float_val"]) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "hello", c.values["string_val"]) } func TestTemplateConfigAssignValuesFromFileForInvalidIntegerValue(t *testing.T) { c := testConfig(t) err := c.assignValuesFromFile("./testdata/config-assign-from-file-invalid-int/config.json") assert.EqualError(t, err, "failed to load config from file ./testdata/config-assign-from-file-invalid-int/config.json: failed to parse property int_val: cannot convert \"abc\" to an integer") } func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *testing.T) { c := testConfig(t) c.values = map[string]any{ "string_val": "this-is-not-overwritten", } err := c.assignValuesFromFile("./testdata/config-assign-from-file/config.json") assert.NoError(t, err) assert.Equal(t, int64(1), c.values["int_val"]) assert.Equal(t, float64(2), c.values["float_val"]) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "this-is-not-overwritten", c.values["string_val"]) } func TestTemplateConfigAssignDefaultValues(t *testing.T) { c := testConfig(t) ctx := context.Background() ctx = root.SetWorkspaceClient(ctx, nil) helpers := loadHelpers(ctx) r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", t.TempDir()) require.NoError(t, err) err = c.assignDefaultValues(r) assert.NoError(t, err) assert.Len(t, c.values, 2) assert.Equal(t, "my_file", c.values["string_val"]) assert.Equal(t, int64(123), c.values["int_val"]) } func TestTemplateConfigValidateValuesDefined(t *testing.T) { c := testConfig(t) c.values = map[string]any{ "int_val": 1, "float_val": 1.0, "bool_val": false, } err := c.validate() assert.EqualError(t, err, "validation for template input parameters failed. no value provided for required property string_val") } func TestTemplateConfigValidateTypeForValidConfig(t *testing.T) { c := testConfig(t) c.values = map[string]any{ "int_val": 1, "float_val": 1.1, "bool_val": true, "string_val": "abcd", } err := c.validate() assert.NoError(t, err) } func TestTemplateConfigValidateTypeForUnknownField(t *testing.T) { c := testConfig(t) c.values = map[string]any{ "unknown_prop": 1, "int_val": 1, "float_val": 1.1, "bool_val": true, "string_val": "abcd", } err := c.validate() assert.EqualError(t, err, "validation for template input parameters failed. property unknown_prop is not defined in the schema") } func TestTemplateConfigValidateTypeForInvalidType(t *testing.T) { c := testConfig(t) c.values = map[string]any{ "int_val": "this-should-be-an-int", "float_val": 1.1, "bool_val": true, "string_val": "abcd", } err := c.validate() assert.EqualError(t, err, "validation for template input parameters failed. incorrect type for property int_val: expected type integer, but value is \"this-should-be-an-int\"") } func TestTemplateValidateSchema(t *testing.T) { var err error toSchema := func(s string) *jsonschema.Schema { return &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "foo": { Type: jsonschema.Type(s), }, }, } } err = validateSchema(toSchema("string")) assert.NoError(t, err) err = validateSchema(toSchema("boolean")) assert.NoError(t, err) err = validateSchema(toSchema("number")) assert.NoError(t, err) err = validateSchema(toSchema("integer")) assert.NoError(t, err) err = validateSchema(toSchema("object")) assert.EqualError(t, err, "property type object is not supported by bundle templates") err = validateSchema(toSchema("array")) assert.EqualError(t, err, "property type array is not supported by bundle templates") } func TestTemplateValidateSchemaVersion(t *testing.T) { version := latestSchemaVersion schema := jsonschema.Schema{ Extension: jsonschema.Extension{ Version: &version, }, } assert.NoError(t, validateSchema(&schema)) version = latestSchemaVersion + 1 schema = jsonschema.Schema{ Extension: jsonschema.Extension{ Version: &version, }, } assert.EqualError(t, validateSchema(&schema), fmt.Sprintf("template schema version %d is not supported by this version of the CLI. Please upgrade your CLI to the latest version", version)) version = 5000 schema = jsonschema.Schema{ Extension: jsonschema.Extension{ Version: &version, }, } assert.EqualError(t, validateSchema(&schema), "template schema version 5000 is not supported by this version of the CLI. Please upgrade your CLI to the latest version") version = 0 schema = jsonschema.Schema{ Extension: jsonschema.Extension{ Version: &version, }, } assert.NoError(t, validateSchema(&schema)) } func TestTemplateEnumValidation(t *testing.T) { schema := jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "abc": { Type: "integer", Enum: []any{1, 2, 3, 4}, }, }, } c := &config{ schema: &schema, values: map[string]any{ "abc": 5, }, } assert.EqualError(t, c.validate(), "validation for template input parameters failed. expected value of property abc to be one of [1 2 3 4]. Found: 5") c = &config{ schema: &schema, values: map[string]any{ "abc": 4, }, } assert.NoError(t, c.validate()) } func TestAssignDefaultValuesWithTemplatedDefaults(t *testing.T) { c := testConfig(t) ctx := context.Background() ctx = root.SetWorkspaceClient(ctx, nil) helpers := loadHelpers(ctx) r, err := newRenderer(ctx, nil, helpers, "./testdata/templated-defaults/template", "./testdata/templated-defaults/library", t.TempDir()) require.NoError(t, err) err = c.assignDefaultValues(r) assert.NoError(t, err) assert.Equal(t, "my_file", c.values["string_val"]) } func TestTemplateSchemaErrorsWithEmptyDescription(t *testing.T) { _, err := newConfig(context.Background(), "./testdata/config-test-schema/invalid-test-schema.json") assert.EqualError(t, err, "template property property-without-description is missing a description") } func testRenderer() *renderer { return &renderer{ config: map[string]any{ "fruit": "apples", }, baseTemplate: template.New(""), } } func TestPromptIsSkippedWhenEmpty(t *testing.T) { c := config{ ctx: context.Background(), values: make(map[string]any), schema: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "always-skip": { Type: "string", Default: "I like {{.fruit}}", Extension: jsonschema.Extension{ SkipPromptIf: &jsonschema.Schema{}, }, }, }, }, } // We should always skip the prompt here. An empty JSON schema by definition // matches all possible configurations. skip, err := c.skipPrompt(jsonschema.Property{ Name: "always-skip", Schema: c.schema.Properties["always-skip"], }, testRenderer()) assert.NoError(t, err) assert.True(t, skip) assert.Equal(t, "I like apples", c.values["always-skip"]) } func TestPromptSkipErrorsWithEmptyDefault(t *testing.T) { c := config{ ctx: context.Background(), values: make(map[string]any), schema: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "no-default": { Type: "string", Extension: jsonschema.Extension{ SkipPromptIf: &jsonschema.Schema{}, }, }, }, }, } _, err := c.skipPrompt(jsonschema.Property{ Name: "no-default", Schema: c.schema.Properties["no-default"], }, testRenderer()) assert.EqualError(t, err, "property no-default has skip_prompt_if set but no default value") } func TestPromptIsSkippedIfValueIsAssigned(t *testing.T) { c := config{ ctx: context.Background(), values: make(map[string]any), schema: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "already-assigned": { Type: "string", Default: "some-default-value", }, }, }, } c.values["already-assigned"] = "some-value" skip, err := c.skipPrompt(jsonschema.Property{ Name: "already-assigned", Schema: c.schema.Properties["already-assigned"], }, testRenderer()) assert.NoError(t, err) assert.True(t, skip) assert.Equal(t, "some-value", c.values["already-assigned"]) } func TestPromptIsSkipped(t *testing.T) { c := config{ ctx: context.Background(), values: make(map[string]any), schema: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "abc": { Type: "string", }, "def": { Type: "integer", }, "xyz": { Type: "string", Default: "hello-world", Extension: jsonschema.Extension{ SkipPromptIf: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "abc": { Const: "foobar", }, "def": { Const: 123, }, }, }, }, }, }, }, } // No skip condition defined. Prompt should not be skipped. skip, err := c.skipPrompt(jsonschema.Property{ Name: "abc", Schema: c.schema.Properties["abc"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) // No values assigned to config. Prompt should not be skipped. skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) assert.NotContains(t, c.values, "xyz") // Values do not match skip condition. Prompt should not be skipped. c.values["abc"] = "foo" c.values["def"] = 123 skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) assert.NotContains(t, c.values, "xyz") // Values do not match skip condition. Prompt should not be skipped. c.values["abc"] = "foobar" c.values["def"] = 1234 skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) assert.NotContains(t, c.values, "xyz") // Values match skip condition. Prompt should be skipped. Default value should // be assigned to "xyz". c.values["abc"] = "foobar" c.values["def"] = 123 skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.True(t, skip) assert.Equal(t, "hello-world", c.values["xyz"]) } func TestPromptIsSkippedAnyOf(t *testing.T) { c := config{ ctx: context.Background(), values: make(map[string]any), schema: &jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{ "abc": { Type: "string", }, "def": { Type: "integer", }, "xyz": { Type: "string", Default: "hello-world", Extension: jsonschema.Extension{ SkipPromptIf: &jsonschema.Schema{ AnyOf: []*jsonschema.Schema{ { Properties: map[string]*jsonschema.Schema{ "abc": { Const: "foobar", }, "def": { Const: 123, }, }, }, { Properties: map[string]*jsonschema.Schema{ "abc": { Const: "barfoo", }, }, }, }, }, }, }, }, }, } // No skip condition defined. Prompt should not be skipped. skip, err := c.skipPrompt(jsonschema.Property{ Name: "abc", Schema: c.schema.Properties["abc"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) // Values do not match skip condition. Prompt should not be skipped. c.values["abc"] = "foobar" c.values["def"] = 1234 skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.False(t, skip) assert.NotContains(t, c.values, "xyz") // Values match skip condition. Prompt should be skipped. Default value should // be assigned to "xyz". c.values["abc"] = "foobar" c.values["def"] = 123 skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.True(t, skip) assert.Equal(t, "hello-world", c.values["xyz"]) // Values match skip condition. Prompt should be skipped. Default value should // be assigned to "xyz". c.values["abc"] = "barfoo" skip, err = c.skipPrompt(jsonschema.Property{ Name: "xyz", Schema: c.schema.Properties["xyz"], }, testRenderer()) assert.NoError(t, err) assert.True(t, skip) assert.Equal(t, "hello-world", c.values["xyz"]) }