package template import ( "context" "fmt" "path/filepath" "testing" "text/template" "github.com/databricks/cli/libs/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTemplateConfigAssignValuesFromFile(t *testing.T) { testDir := "./testdata/config-assign-from-file" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) if 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 TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *testing.T) { testDir := "./testdata/config-assign-from-file" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) c.values = map[string]any{ "string_val": "this-is-not-overwritten", } err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) if 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 TestTemplateConfigAssignValuesFromFileForInvalidIntegerValue(t *testing.T) { testDir := "./testdata/config-assign-from-file-invalid-int" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) assert.EqualError(t, err, fmt.Sprintf("failed to load config from file %s: failed to parse property int_val: cannot convert \"abc\" to an integer", filepath.Join(testDir, "config.json"))) } func TestTemplateConfigAssignValuesFromFileFiltersPropertiesNotInTheSchema(t *testing.T) { testDir := "./testdata/config-assign-from-file-unknown-property" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) assert.NoError(t, err) // assert only the known property is loaded assert.Len(t, c.values, 1) assert.Equal(t, "i am a known property", c.values["string_val"]) } func TestTemplateConfigAssignValuesFromDefaultValues(t *testing.T) { testDir := "./testdata/config-assign-from-default-value" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) r, err := newRenderer(ctx, nil, nil, "./testdata/empty/template", "./testdata/empty/library", t.TempDir()) require.NoError(t, err) err = c.assignDefaultValues(r) if assert.NoError(t, err) { assert.Equal(t, int64(123), c.values["int_val"]) assert.Equal(t, float64(123), c.values["float_val"]) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "hello", c.values["string_val"]) } } func TestTemplateConfigAssignValuesFromTemplatedDefaultValues(t *testing.T) { testDir := "./testdata/config-assign-from-templated-default-value" ctx := context.Background() c, err := newConfig(ctx, filepath.Join(testDir, "schema.json")) require.NoError(t, err) r, err := newRenderer(ctx, nil, nil, filepath.Join(testDir, "template/template"), filepath.Join(testDir, "template/library"), t.TempDir()) require.NoError(t, err) // Note: only the string value is templated. // The JSON schema package doesn't allow using a string default for integer types. err = c.assignDefaultValues(r) if assert.NoError(t, err) { assert.Equal(t, int64(123), c.values["int_val"]) assert.Equal(t, float64(123), c.values["float_val"]) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "world", c.values["string_val"]) } } func TestTemplateConfigValidateValuesDefined(t *testing.T) { ctx := context.Background() c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json") require.NoError(t, err) 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) { ctx := context.Background() c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json") require.NoError(t, err) 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) { ctx := context.Background() c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json") require.NoError(t, err) 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) { ctx := context.Background() c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json") require.NoError(t, err) 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 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, }, }, Required: []string{"abc", "def"}, }, }, }, }, }, } // 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, }, }, Required: []string{"abc", "def"}, }, { 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 = map[string]any{ "abc": "foobar", "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") // def is missing value. Prompt should not be skipped. c.values = map[string]any{ "abc": "foobar", } _, 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") // abc is missing value. Prompt should be skipped because abc is optional // in second condition. c.values = map[string]any{ "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 = map[string]any{ "abc": "foobar", "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 = map[string]any{ "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"]) }