Skip to content

Commit 4af1675

Browse files
authored
feat: added schematics unit test support (#307)
* feat: new package testschematic with create tar function * feat: add TestSchematicOptions Changes to support adding a TestSchematicOptions and factory, including small changes to testhelper private methods to public to avoid duplication * feat: initial working schematic creation initial working schematic workspace creation, including env and input variables, and tar upload of terraform * feat: added wait for job completion and status * refactor: moved all schematics code to own file * feat: full end-to-end schematics test working * feat: full working schematics testing * refactor: fixed some error messages * docs: added function header comments * fix: pointer bug and interface cleanup * fix: add test service for options, for mocking * test: added full unit tests for schematics service * feat: add schematic options for working folder and tf version * fix: tar subdirectories and optional tf version for workspace * fix: schematic template folder not supported with tar upload * feat: schematics support for netrc and env settings * fix: added parent directory to schematic tar file * fix: schematic GetRefreshToken always new token Changed GetRefreshToken for schematics api to always request a new token when called. Resulted in not needing to persist token at service level, and instead opt for IamAuthenticator mocking for tests * refactor: remove api version from file names * refactor: changed references of schematicsv1 remove version number Dropped the version number notation in all schematics packages by using alias * refactor: move common helper functions to new package located helper functions to a new common package to be shared with all other packages
1 parent bd9652d commit 4af1675

18 files changed

+2142
-249
lines changed

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2023-01-13T13:18:01Z",
6+
"generated_at": "2023-01-17T15:29:43Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -82,15 +82,15 @@
8282
"hashed_secret": "892bd503fb45f6fcafb1c7003d88291fc0b20208",
8383
"is_secret": false,
8484
"is_verified": false,
85-
"line_number": 104,
85+
"line_number": 115,
8686
"type": "Secret Keyword",
8787
"verified_result": null
8888
},
8989
{
9090
"hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030",
9191
"is_secret": false,
9292
"is_verified": false,
93-
"line_number": 165,
93+
"line_number": 176,
9494
"type": "Secret Keyword",
9595
"verified_result": null
9696
}

cloudinfo/service.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ type CloudInfoService struct {
2626
lock sync.Mutex
2727
}
2828

29+
// interface for the cloudinfo service (can be mocked in tests)
30+
type CloudInfoServiceI interface {
31+
GetLeastVpcTestRegion() (string, error)
32+
GetLeastVpcTestRegionWithoutActivityTracker() (string, error)
33+
GetLeastPowerConnectionZone() (string, error)
34+
LoadRegionPrefsFromFile(string) error
35+
HasRegionData() bool
36+
RemoveRegionForTest(string)
37+
GetThreadLock() *sync.Mutex
38+
}
39+
2940
// CloudInfoServiceOptions structure used as input params for service constructor.
3041
type CloudInfoServiceOptions struct {
3142
ApiKey string

common/general.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package common
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"os/exec"
7+
"reflect"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// GetRequiredEnvVars returns a map containing required environment variables and their values
15+
// Fails the test if any are missing
16+
func GetRequiredEnvVars(t *testing.T, variableNames []string) map[string]string {
17+
var missingVariables []string
18+
envVars := make(map[string]string)
19+
20+
for _, variableName := range variableNames {
21+
val, present := os.LookupEnv(variableName)
22+
if present {
23+
envVars[variableName] = val
24+
} else {
25+
missingVariables = append(missingVariables, variableName)
26+
}
27+
}
28+
require.Empty(t, missingVariables, "The following environment variables must be set: %v", missingVariables)
29+
30+
return envVars
31+
}
32+
33+
// GitRootPath gets the path to the current git repos root directory
34+
func GitRootPath(fromPath string) (string, error) {
35+
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
36+
cmd.Dir = fromPath
37+
path, err := cmd.Output()
38+
39+
if err != nil {
40+
return "", err
41+
}
42+
return strings.TrimSpace(string(path)), nil
43+
}
44+
45+
// GetBeforeAfterDiff takes a JSON string as input and returns a string with the differences
46+
// between the "before" and "after" objects in the JSON.
47+
//
48+
// For example, given the JSON string:
49+
//
50+
// {"before": {"a": 1, "b": 2}, "after": {"a": 2, "b": 3}}
51+
//
52+
// the function would return the string:
53+
//
54+
// "Before: {"b": 2}\nAfter: {"a": 2, "b": 3}"
55+
func GetBeforeAfterDiff(jsonString string) string {
56+
// Parse the JSON string into a map
57+
var jsonMap map[string]interface{}
58+
err := json.Unmarshal([]byte(jsonString), &jsonMap)
59+
if err != nil {
60+
return "Error: unable to parse JSON string"
61+
}
62+
63+
// Get the "before" and "after" values from the map
64+
before, beforeOk := jsonMap["before"]
65+
after, afterOk := jsonMap["after"]
66+
if !beforeOk || !afterOk {
67+
return "Error: missing 'before' or 'after' key in JSON"
68+
}
69+
70+
// Check if the "before" and "after" values are objects
71+
beforeObject, beforeOk := before.(map[string]interface{})
72+
if !beforeOk {
73+
return "Error: 'before' value is not an object"
74+
}
75+
afterObject, afterOk := after.(map[string]interface{})
76+
if !afterOk {
77+
return "Error: 'after' value is not an object"
78+
}
79+
80+
// Find the differences between the two objects
81+
diffsBefore := make(map[string]interface{})
82+
for key, value := range beforeObject {
83+
if !reflect.DeepEqual(afterObject[key], value) {
84+
diffsBefore[key] = value
85+
}
86+
}
87+
88+
// Convert the diffs map to a JSON string
89+
diffsJson, err := json.Marshal(diffsBefore)
90+
if err != nil {
91+
return "Error: unable to convert diffs to JSON"
92+
}
93+
94+
// Find the differences between the two objects
95+
diffsAfter := make(map[string]interface{})
96+
for key, value := range afterObject {
97+
if !reflect.DeepEqual(beforeObject[key], value) {
98+
diffsAfter[key] = value
99+
}
100+
}
101+
102+
// Convert the diffs map to a JSON string
103+
diffsJson2, err := json.Marshal(diffsAfter)
104+
if err != nil {
105+
return "Error: unable to convert diffs2 to JSON"
106+
}
107+
108+
return "Before: " + string(diffsJson) + "\nAfter: " + string(diffsJson2)
109+
}
110+
111+
// overwriting duplicate keys
112+
func MergeMaps(maps ...map[string]interface{}) map[string]interface{} {
113+
result := make(map[string]interface{})
114+
for _, m := range maps {
115+
for k, v := range m {
116+
result[k] = v
117+
}
118+
}
119+
return result
120+
}
121+
122+
// Adds value to map[key] only if value != compareValue
123+
func ConditionalAdd(amap map[string]interface{}, key string, value string, compareValue string) {
124+
if value != compareValue {
125+
amap[key] = value
126+
}
127+
}
128+
129+
// ConvertArrayToJsonString is a helper function that will take an array of Golang data types, and return a string
130+
// of the array formatted as a JSON array.
131+
// Helpful to convert Golang arrays into a format that Terraform can consume.
132+
func ConvertArrayToJsonString(arr interface{}) (string, error) {
133+
// first marshal array into json compatible
134+
json, jsonErr := json.Marshal(arr)
135+
if jsonErr != nil {
136+
return "", jsonErr
137+
}
138+
139+
// take json array, wrap as one string, and escape any double quotes inside
140+
s := string(json)
141+
142+
return s, nil
143+
}
144+
145+
// IsArray is a simple helper function that will determine if a given Golang value is a slice or array.
146+
func IsArray(v interface{}) bool {
147+
148+
theType := reflect.TypeOf(v).Kind()
149+
150+
if (theType == reflect.Slice) || (theType == reflect.Array) {
151+
return true
152+
}
153+
154+
return false
155+
}
156+
157+
// StrArrayContains is a helper function that will check an array and see if a value is already present
158+
func StrArrayContains(arr []string, val string) bool {
159+
for _, arrVal := range arr {
160+
if arrVal == val {
161+
return true
162+
}
163+
}
164+
165+
return false
166+
}

common/general_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGetRequiredEnvVarsSuccess(t *testing.T) {
10+
t.Setenv("A_REQUIRED_VARIABLE", "The Value")
11+
t.Setenv("ANOTHER_VARIABLE", "Another Value")
12+
13+
expected := make(map[string]string)
14+
expected["A_REQUIRED_VARIABLE"] = "The Value"
15+
expected["ANOTHER_VARIABLE"] = "Another Value"
16+
17+
assert.Equal(t, expected, GetRequiredEnvVars(t, []string{"A_REQUIRED_VARIABLE", "ANOTHER_VARIABLE"}))
18+
}
19+
20+
func TestGetRequiredEnvVarsEmptyInput(t *testing.T) {
21+
22+
expected := make(map[string]string)
23+
assert.Equal(t, expected, GetRequiredEnvVars(t, []string{}))
24+
}
25+
26+
func TestGetBeforeAfterDiffValidInput(t *testing.T) {
27+
jsonString := `{"before": {"a": 1, "b": 2}, "after": {"a": 2, "b": 3}}`
28+
expected := "Before: {\"a\":1,\"b\":2}\nAfter: {\"a\":2,\"b\":3}"
29+
result := GetBeforeAfterDiff(jsonString)
30+
if result != expected {
31+
t.Errorf("TestGetBeforeAfterDiffValidInput(%q) returned %q, expected %q", jsonString, result, expected)
32+
}
33+
}
34+
35+
func TestGetBeforeAfterDiffMissingBeforeKey(t *testing.T) {
36+
jsonString := `{"after": {"a": 1, "b": 2}}`
37+
expected := "Error: missing 'before' or 'after' key in JSON"
38+
result := GetBeforeAfterDiff(jsonString)
39+
if result != expected {
40+
t.Errorf("TestGetBeforeAfterDiffMissingBeforeKey(%q) returned %q, expected %q", jsonString, result, expected)
41+
}
42+
}
43+
44+
func TestGetBeforeAfterDiffNonObjectBeforeValue(t *testing.T) {
45+
jsonString := `{"before": ["a", "b"], "after": {"a": 1, "b": 2}}`
46+
expected := "Error: 'before' value is not an object"
47+
result := GetBeforeAfterDiff(jsonString)
48+
if result != expected {
49+
t.Errorf("TestGetBeforeAfterDiffNonObjectBeforeValue(%q) returned %q, expected %q", jsonString, result, expected)
50+
}
51+
}
52+
53+
func TestGetBeforeAfterDiffNonObjectAfterValue(t *testing.T) {
54+
jsonString := `{"before": {"a": 1, "b": 2}, "after": ["a", "b"]}`
55+
expected := "Error: 'after' value is not an object"
56+
result := GetBeforeAfterDiff(jsonString)
57+
if result != expected {
58+
t.Errorf("TestGetBeforeAfterDiffNonObjectAfterValue(%q) returned %q, expected %q", jsonString, result, expected)
59+
}
60+
}
61+
62+
func TestGetBeforeAfterDiffInvalidJSON(t *testing.T) {
63+
jsonString := `{"before": {"a": 1, "b": 2}, "after": {"a": 1, "b": 2}`
64+
expected := "Error: unable to parse JSON string"
65+
result := GetBeforeAfterDiff(jsonString)
66+
if result != expected {
67+
t.Errorf("TestGetBeforeAfterDiffInvalidJSON(%q) returned %q, expected %q", jsonString, result, expected)
68+
}
69+
}
70+
71+
func TestConvertArrayJson(t *testing.T) {
72+
73+
t.Run("GoodArray", func(t *testing.T) {
74+
goodArr := []interface{}{
75+
"hello",
76+
true,
77+
99,
78+
"bye",
79+
}
80+
goodStr, goodErr := ConvertArrayToJsonString(goodArr)
81+
if assert.NoError(t, goodErr, "error converting array") {
82+
assert.NotEmpty(t, goodStr)
83+
}
84+
})
85+
86+
t.Run("NilValue", func(t *testing.T) {
87+
nullVal, nullErr := ConvertArrayToJsonString(nil)
88+
if assert.NoError(t, nullErr) {
89+
assert.Equal(t, "null", nullVal)
90+
}
91+
})
92+
93+
t.Run("NullPointer", func(t *testing.T) {
94+
var intPtr *int
95+
ptrVal, ptrErr := ConvertArrayToJsonString(intPtr)
96+
if assert.NoError(t, ptrErr) {
97+
assert.Equal(t, "null", ptrVal)
98+
}
99+
})
100+
}
101+
102+
func TestIsArray(t *testing.T) {
103+
104+
t.Run("IsSlice", func(t *testing.T) {
105+
slice := []int{1, 2, 3}
106+
isSlice := IsArray(slice)
107+
assert.True(t, isSlice)
108+
})
109+
110+
t.Run("IsArray", func(t *testing.T) {
111+
arr := [3]int{1, 2, 3}
112+
isArr := IsArray(arr)
113+
assert.True(t, isArr)
114+
})
115+
116+
t.Run("TryString", func(t *testing.T) {
117+
val := "hello"
118+
is := IsArray(val)
119+
assert.False(t, is)
120+
})
121+
122+
t.Run("TryBool", func(t *testing.T) {
123+
bval := true
124+
bis := IsArray(bval)
125+
assert.False(t, bis)
126+
})
127+
128+
t.Run("TryNumber", func(t *testing.T) {
129+
nval := 99.99
130+
nis := IsArray(nval)
131+
assert.False(t, nis)
132+
})
133+
134+
t.Run("TryStruct", func(t *testing.T) {
135+
type TestObject struct {
136+
prop1 string
137+
prop2 int
138+
}
139+
obj := &TestObject{"hello", 99}
140+
sis := IsArray(*obj)
141+
assert.False(t, sis)
142+
})
143+
}

testhelper/travis.go renamed to common/travis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package testhelper
1+
package common
22

33
import (
44
"os"

testhelper/travis_test.go renamed to common/travis_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
package testhelper
1+
package common
22

33
import (
4-
"github.com/stretchr/testify/assert"
54
"strings"
65
"testing"
6+
7+
"github.com/stretchr/testify/assert"
78
)
89

910
type travisVariables struct {

0 commit comments

Comments
 (0)