asteris-llc/converge

View on GitHub
parse/node_test.go

Summary

Maintainability
C
1 day
Test Coverage
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package parse_test

import (
    "errors"
    "fmt"
    "sort"
    "testing"

    "github.com/asteris-llc/converge/parse"
    "github.com/hashicorp/hcl"
    "github.com/hashicorp/hcl/hcl/ast"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func fromString(content string) (*parse.Node, error) {
    obj, err := hcl.ParseString(content)
    if err != nil {
        return nil, err
    }

    list, ok := obj.Node.(*ast.ObjectList)
    if !ok {
        return nil, errors.New("not an objectlist")
    }

    return parse.NewNode(list.Items[0]), nil
}

func validateTable(t *testing.T, input, errMsg string) {
    node, err := fromString(input)
    require.NoError(t, err) // must be syntactically valid

    err = node.Validate()
    if assert.Error(t, err) {
        assert.EqualError(t, err, errMsg)
    }
}

func TestNodeValidate(t *testing.T) {
    // everything about this should be valid
    t.Parallel()

    node, err := fromString(`task "x" {}`)
    assert.NoError(t, err)
    assert.NoError(t, node.Validate())
}

func TestNodeValidateNoName(t *testing.T) {
    // missing name, which is invalid
    t.Parallel()

    validateTable(t, `x {}`, "1:1: missing name")
}

func TestNodeValidateModuleMissingNameOrSource(t *testing.T) {
    // missing name/source in a module call, which is invalid
    t.Parallel()

    validateTable(t, `module x {}`, "1:1: missing source or name in module call")
}

func TestNodeValidateTooManyKeys(t *testing.T) {
    // too many keys is a problem
    t.Parallel()

    validateTable(t, `task x y {}`, "1:1: too many keys")
}

func TestNodeValidateTooManyKeysModule(t *testing.T) {
    // too many keys is a problem in modules too!
    t.Parallel()

    validateTable(t, `module x y z {}`, "1:1: too many keys")
}

// TestNodeCase tests various scenarios where the node is a case statement
func TestNodeCase(t *testing.T) {
    t.Parallel()
    t.Run("when no name or predicate", func(t *testing.T) {
        validateTable(t, `case {}`, "1:1: missing name")
    })

    t.Run("when no name or predicate", func(t *testing.T) {
        validateTable(t, `case x {}`, "1:1: missing name or predicate in case")
    })

    t.Run("when too many keys", func(t *testing.T) {
        validateTable(t, `case x y z {}`, "1:1: too many keys")
    })
}

// TestNodeValidateName tests to ensure that we only allow supported names for
// resources.
func TestNodeValidateName(t *testing.T) {
    t.Parallel()
    t.Run("when valid", func(t *testing.T) {
        t.Parallel()
        t.Run("alpha", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "abc" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "もしもし" { }`)
            assert.NoError(t, err)
            _, err = fromString(`test "ڛ" { }`)
            assert.NoError(t, err)
        })
        t.Run("numbers", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "abc123" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "abc123xyz" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "8080" { value = 7 }`)
            assert.NoError(t, err)
        })
        t.Run("dashes", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "a-" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "-a-" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a-a" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a--" { value = 7 }`)
            assert.NoError(t, err)
        })
        t.Run("dots", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "a." { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a.a" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a.." { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "..a.." { value = 7 }`)
            assert.NoError(t, err)
        })
        t.Run("underscores", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "a_" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a_a" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a__" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "__a" { value = 7 }`)
            assert.NoError(t, err)
        })
        t.Run("heterogenous", func(t *testing.T) {
            t.Parallel()
            _, err := fromString(`test "a_" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a_-" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a1_2" { value = 7 }`)
            assert.NoError(t, err)
            _, err = fromString(`test "a1-2" { value = 7 }`)
            assert.NoError(t, err)
        })
    })
    t.Run("when invalid character", func(t *testing.T) {
        t.Parallel()
        t.Run("when space", func(t *testing.T) {
            t.Parallel()
            spaceChars := []string{" ", "\t", "\v", "\f", "\r"}
            for _, space := range spaceChars {
                hcl := fmt.Sprintf("test \"abc%sdef\" { }", space)
                validateTable(t, hcl, "1:1: resource name may not contain spaces")
            }
        })
        t.Run("When other invalid character", func(t *testing.T) {
            t.Parallel()
            testChars := []string{
                "!",
                "/",
                "+",
                "🂡",
                "💩",
                "}",
            }
            for _, char := range testChars {
                hcl := fmt.Sprintf("test \"abc%sdef\" { }", char)
                msg := fmt.Sprintf("1:1: invalid character(s) in resource name: [%v]; valid characters are unicode letters and numbers, dashes '-', underscores '_', and dots '.'", string((char)))
                validateTable(t, hcl, msg)
            }
        })
    })
}

// TestNodeDefault tests various scenarios where the node is a default statement
func TestNodeDefault(t *testing.T) {
    t.Parallel()
    t.Run("when no name or predicate", func(t *testing.T) {
        node, err := fromString(`default {}`)
        assert.NoError(t, err)
        assert.Equal(t, "default", node.Kind())
    })

    t.Run("when no name or predicate", func(t *testing.T) {
        validateTable(t, `default x {}`, "1:1: too many keys")
    })

    t.Run("when too many keys", func(t *testing.T) {
        validateTable(t, `default x y z {}`, "1:1: too many keys")
    })
}

func TestNodeKind(t *testing.T) {
    t.Parallel()

    node, err := fromString(`task "x" {}`)
    assert.NoError(t, err)
    assert.Equal(t, "task", node.Kind())
}

func TestNodeName(t *testing.T) {
    t.Parallel()

    node, err := fromString(`task "x" {}`)
    assert.NoError(t, err)
    assert.Equal(t, "x", node.Name())
}

func TestNodeIsModule(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module "source" "name" {}`)
    assert.NoError(t, err)
    assert.True(t, node.IsModule())
}

func TestNodeIsntModule(t *testing.T) {
    t.Parallel()

    node, err := fromString(`task "name" {}`)
    assert.NoError(t, err)
    assert.False(t, node.IsModule())
}

func TestNodeSource(t *testing.T) {
    t.Parallel()

    node, err := fromString("module x y {}")
    assert.NoError(t, err)
    assert.Equal(t, "x", node.Source())
}

// TestNodeGroup verifies that a group can be parsed
func TestNodeGroup(t *testing.T) {
    t.Parallel()

    node, err := fromString(`task "x" { group = "somegroup" }`)
    assert.NoError(t, err)
    assert.Equal(t, "somegroup", node.Group())
}

func TestNodeGet(t *testing.T) {
    t.Parallel()

    node, err := fromString("module x y { a = 1 }")
    require.NoError(t, err)

    val, err := node.Get("a")
    assert.NoError(t, err)
    assert.Equal(t, 1, val)
}

func TestNodeGetBad(t *testing.T) {
    t.Parallel()

    node, err := fromString("module x y {}")
    require.NoError(t, err)

    val, err := node.Get("a")
    assert.Nil(t, val)
    assert.Equal(t, err, parse.ErrNotFound)
}

func TestNodeGetString(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module x y { a = "a" }`)
    require.NoError(t, err)

    val, err := node.GetString("a")
    assert.NoError(t, err)
    assert.Equal(t, "a", val)
}

func TestNodeGetStringBad(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module x y { a = 1 }`)
    require.NoError(t, err)

    val, err := node.GetString("a")
    assert.Empty(t, val)
    if assert.Error(t, err) {
        assert.EqualError(
            t,
            err,
            `"a" is not a string, it is an int`,
        )
    }
}

func TestNodeGetStringSlice(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module x y { a = ["a", "b"] }`)
    require.NoError(t, err)

    val, err := node.GetStringSlice("a")
    assert.NoError(t, err)
    assert.Equal(t, []string{"a", "b"}, val)
}

func TestNodeGetStringSliceBad(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module x y { a = 1 }`)
    require.NoError(t, err)

    val, err := node.GetStringSlice("a")
    assert.Empty(t, val)
    if assert.Error(t, err) {
        assert.EqualError(
            t,
            err,
            `"a" is not a slice, it is an int`,
        )
    }
}

func TestNodeGetStringSliceBadItem(t *testing.T) {
    t.Parallel()

    node, err := fromString(`module x y { a = [ 1 ] }`)
    require.NoError(t, err)

    val, err := node.GetStringSlice("a")
    assert.Empty(t, val)
    if assert.Error(t, err) {
        assert.EqualError(
            t,
            err,
            `"a.0" is not a string, it is an int`,
        )
    }
}

func TestNodeGetStrings(t *testing.T) {
    t.Parallel()

    node, err := fromString(`
module x y {
    fst = "fst"
    snd = "snd"
}
`)
    require.NoError(t, err)

    vals, err := node.GetStrings()
    assert.NoError(t, err)

    sort.Strings(vals)
    assert.Equal(t, []string{"fst", "snd"}, vals)
}

// TestNodeGetStringsWithMap will test the special case of a node with a map to
// ensure the keys are also considered valid strings
func TestNodeGetStringsWithMap(t *testing.T) {
    t.Parallel()
    node, err := fromString(`
module x y {
    aMap {
        "key" = "value"
        nestedMap {
            "nestedKey1" = "nestedValue1"
        }
    }
}
`)
    require.NoError(t, err)

    vals, err := node.GetStrings()
    assert.NoError(t, err)

    sort.Strings(vals)
    assert.Equal(t, []string{"key", "nestedKey1", "nestedMap", "nestedValue1", "value"}, vals)
}