Skip to content

Commit

Permalink
Merge pull request #25 from TykTechnologies/TT-11478
Browse files Browse the repository at this point in the history
[TT-11478] Fixing EnvsHandler
  • Loading branch information
mativm02 authored May 13, 2024
2 parents 0706cf3 + ae27572 commit c440378
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 87 deletions.
2 changes: 1 addition & 1 deletion exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (v *Viewer) DetailedConfigHandler(rw http.ResponseWriter, r *http.Request)

// EnvsHandler expose the environment variables of the configuration struct
func (v *Viewer) EnvsHandler(rw http.ResponseWriter, r *http.Request) {
if v.config == nil {
if v.envs == nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
Expand Down
54 changes: 28 additions & 26 deletions exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,7 @@ func TestDetailedConfigHandler(t *testing.T) {

func TestEnvsHandler(t *testing.T) {
tcs := []struct {
testName string

testName string
givenConfig interface{}
givenPrefix string
queryParamVal string
Expand All @@ -265,7 +264,7 @@ func TestEnvsHandler(t *testing.T) {
"field_value",
},
expectedStatusCode: http.StatusOK,
expectedJSONOutput: fmt.Sprintln(`["FIELDNAME:field_value"]`),
expectedJSONOutput: fmt.Sprintln(`["FIELDNAME=field_value"]`),
},
{
testName: "simple struct with prefix",
Expand All @@ -276,22 +275,21 @@ func TestEnvsHandler(t *testing.T) {
},
givenPrefix: "TEST_",
expectedStatusCode: http.StatusOK,
expectedJSONOutput: fmt.Sprintln(`["TEST_FIELDNAME:field_value"]`),
expectedJSONOutput: fmt.Sprintln(`["TEST_FIELDNAME=field_value"]`),
},
{
testName: "complex struct",
givenConfig: complexStruct,
expectedStatusCode: http.StatusOK,
expectedJSONOutput: fmt.Sprintln(
`["NAME=name_value",` +
`"DATA_OBJECT1=1",` +
`"DATA_OBJECT2=true",` +
`"METADATA_ID=99",` +
`"METADATA_VALUE=key99",` +
`"OMITTEDVALUE=''"]`,
),
},
// TODO: Uncomment this test once this issue is addressed:
// https://github.com/TykTechnologies/structviewer/issues/7
// {
// testName: "complex struct struct",
// givenConfig: complexStruct,
// expectedStatusCode: http.StatusOK,
// expectedJSONOutput: fmt.Sprintln(
// `["NAME:name_value",` +
// `"DATA_OBJECT1:1",` +
// `"DATA_OBJECT2:true",` +
// `"METADATA:map[key_99:{99 key99}]",` +
// `"OMITTEDVALUE:"]`,
// ),
// },
{
testName: "valid field of complexStruct via query param",
givenConfig: complexStruct,
Expand Down Expand Up @@ -330,8 +328,6 @@ func TestEnvsHandler(t *testing.T) {

for _, tc := range tcs {
t.Run(tc.testName, func(t *testing.T) {
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
// pass 'nil' as the third parameter.
req, err := http.NewRequest("GET", "/", nil)
assert.NoError(t, err)

Expand All @@ -341,19 +337,25 @@ func TestEnvsHandler(t *testing.T) {
helper, err := New(&structViewerConfig, tc.givenPrefix)
assert.NoError(t, err, "failed to instantiate viewer")

// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
rr := httptest.NewRecorder()
handler := http.HandlerFunc(helper.EnvsHandler)

// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
// directly and pass in our Request and ResponseRecorder.
handler.ServeHTTP(rr, req)

// Check the status code is what we expect.
assert.Equal(t, tc.expectedStatusCode, rr.Code)

// Check the response body is what we expect.
assert.JSONEq(t, tc.expectedJSONOutput, rr.Body.String())
// Determine whether the expected output is an array by trying to unmarshal it into a slice
var expectedArray, actualArray []string
expectedArrayErr := json.Unmarshal([]byte(tc.expectedJSONOutput), &expectedArray)
actualArrayErr := json.Unmarshal(rr.Body.Bytes(), &actualArray)

if expectedArrayErr == nil && actualArrayErr == nil {
// Both JSON strings are arrays; compare using unordered comparison
assert.ElementsMatch(t, expectedArray, actualArray)
} else {
// Not arrays, compare as ordered JSON strings
assert.JSONEq(t, tc.expectedJSONOutput, rr.Body.String())
}
})
}
}
72 changes: 29 additions & 43 deletions internal/test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,41 @@ import (
"github.com/TykTechnologies/structviewer"
)

// InnerStructType represents an inner struct.
type InnerStructType struct {
// DummyAddr represents an address.
DummyAddr string `json:"dummy_addr"`
type complexType struct {
Data struct {
Object1 int `json:"object_1,omitempty"`
Object2 bool `json:"object_2,omitempty"`
} `json:"data"`
Metadata map[string]struct {
ID int `json:"id,omitempty"`
Value string `json:"value,omitempty"`
} `json:"metadata,omitempty"`
Random map[int]string `json:"random,omitempty"`
}

// StructType represents a struct type.
type StructType struct {
// Enable represents status.
Enable bool `json:"enable"`
// Inner is an inner struct.
Inner InnerStructType `json:"inner"`
}

type testStruct struct {
// Exported represents a sample exported field.
Exported string `json:"exported"`
notExported bool //lint:ignore U1000 Unused field is used for testing purposes.

// StrField is a struct field.
StrField struct {
// Test is a field of struct type.
Test string `json:"test"`
Other struct {
// OtherTest represents a field of sub-struct.
OtherTest bool `json:"other_test"`
nonEmbedded string
} `json:"other"`
} `json:"str_field"`

// ST is another struct type.
ST StructType `json:"st"`

// JSONExported includes a JSON tag.
JSONExported int `json:"json_exported" structviewer:"obfuscate"`
var complexStruct = complexType{
Data: struct {
Object1 int `json:"object_1,omitempty"`
Object2 bool `json:"object_2,omitempty"`
}{
Object1: 1,
Object2: true,
},
Metadata: map[string]struct {
ID int `json:"id,omitempty"`
Value string `json:"value,omitempty"`
}{
"key_99": {ID: 99, Value: "key99"},
},
Random: map[int]string{
1: "one",
2: "two",
},
}

func main() {
config := &structviewer.Config{
Object: &testStruct{
Exported: "exported_value",
ST: StructType{
Enable: true,
Inner: InnerStructType{
DummyAddr: "dummy_addr_value",
},
},
JSONExported: 10,
},
Object: complexStruct,
Path: "./main.go",
ParseComments: true,
}
Expand Down
83 changes: 71 additions & 12 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,38 @@ import (
// ParseEnvs parse Viewer config field, generating a string slice of prefix+key:value of each config field
func (v *Viewer) ParseEnvs() []string {
var envs []string
envVars := v.envs
envVars := v.configMap

if len(envVars) == 0 {
envVars = parseEnvs(v.config, v.prefix, "")
for _, envVar := range envVars {
envs = append(envs, generateEnvStrings(envVar)...)
}

for i := range envVars {
env := envVars[i]
envs = append(envs, v.prefix+env.String())
return envs
}

func generateEnvStrings(e *EnvVar) []string {
var strEnvs []string

if e.isStruct {
typedEnv, ok := e.Value.(map[string]*EnvVar)
if !ok {
return []string{""}
}

for _, v := range typedEnv {
strEnvs = append(strEnvs, generateEnvStrings(v)...)
}

return strEnvs
}

return envs
if e.Value == "" || e.Value == nil {
e.Value = `''`
}

strEnvs = append(strEnvs, fmt.Sprintf("%v=%v", e.Env, e.Value))

return strEnvs
}

// EnvNotation takes JSON notation of a configuration field (e.g, 'listen_port') and returns EnvVar object of the given
Expand Down Expand Up @@ -190,7 +210,6 @@ func (v *Viewer) get(field string, envs []*EnvVar) *EnvVar {

return nil
}

func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
var envs []*EnvVar

Expand All @@ -201,7 +220,7 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
newEnv := &EnvVar{}
newEnv.setKey(field)

// Ensuring that the configField ends with a single dot (only if it is not empty)
// Ensure that the configField ends with a single dot (only if it is not empty)
if configField != "" && configField[len(configField)-1] != '.' {
configField += "."
}
Expand All @@ -217,8 +236,48 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
newEnv.ConfigField = ""
newEnv.isStruct = true

envs = append(envs, newEnv)
} else if reflect.ValueOf(field.Value()).Kind() == reflect.Map {
v := reflect.ValueOf(field.Value())
keys := v.MapKeys()

kvEnvVar := map[string]*EnvVar{}

for _, key := range keys {
value := v.MapIndex(key).Interface()

// Handle different key types by converting to a string representation
keyStr := fmt.Sprintf("%v", key)
mapEnv := &EnvVar{
key: keyStr,
field: keyStr,
}

if reflect.TypeOf(value).Kind() == reflect.Struct {
// Recursively process structs
envsInner := parseEnvs(value, prefix+newEnv.key+"_", configField+newEnv.ConfigField)
for i := range envsInner {
kvEnvVar[envsInner[i].field] = envsInner[i]
}
} else {
// Directly assign other map values to `mapEnv`
mapEnv.Value = value
envSuffix := strings.ToUpper(strings.ReplaceAll(keyStr, "_", ""))
mapEnv.Env = prefix + newEnv.key + "_" + envSuffix
mapEnv.ConfigField = configField + newEnv.ConfigField + "." + keyStr
mapEnv.Obfuscated = getPointerBool(false)

kvEnvVar[keyStr] = mapEnv
}
}

newEnv.Value = kvEnvVar
newEnv.ConfigField = ""
newEnv.isStruct = true

envs = append(envs, newEnv)
} else {
// Use the existing `setValue` function to assign the value
newEnv.setValue(field)
newEnv.Env = prefix + newEnv.key
newEnv.ConfigField = configField + newEnv.ConfigField
Expand All @@ -227,9 +286,9 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
if field.IsZero() && field.Tag(StructViewerTag) == "obfuscate" {
newEnv.Obfuscated = getPointerBool(true)
}
}

envs = append(envs, newEnv)
envs = append(envs, newEnv)
}
}
}

Expand Down Expand Up @@ -310,7 +369,7 @@ type EnvVar struct {

// String returns a key:value string from EnvVar
func (ev EnvVar) String() string {
return fmt.Sprintf("%s:%s", ev.key, ev.Value)
return fmt.Sprintf("%s:%s", ev.Env, ev.Value)
}

func (ev *EnvVar) setKey(field *structs.Field) {
Expand Down
34 changes: 29 additions & 5 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestParseEnvsValues(t *testing.T) {
Key: "Value",
},
expectedLen: 1,
expectedEnvs: []string{"KEY:Value"},
expectedEnvs: []string{"KEY=Value"},
},
{
testName: "KEY:VALUE with json tag",
Expand All @@ -99,7 +99,7 @@ func TestParseEnvsValues(t *testing.T) {
Key: "Value",
},
expectedLen: 1,
expectedEnvs: []string{"JSONNAME:Value"},
expectedEnvs: []string{"JSONNAME=Value"},
},
{
testName: "KEY:VALUE with json tag and omitempty",
Expand All @@ -109,7 +109,7 @@ func TestParseEnvsValues(t *testing.T) {
Key: "Value",
},
expectedLen: 1,
expectedEnvs: []string{"JSONNAME:Value"},
expectedEnvs: []string{"JSONNAME=Value"},
},
{
testName: "KEY:VALUE with json '-' tag",
Expand All @@ -119,7 +119,30 @@ func TestParseEnvsValues(t *testing.T) {
Key: "Value",
},
expectedLen: 1,
expectedEnvs: []string{"KEY:Value"},
expectedEnvs: []string{"KEY=Value"},
},
{
testName: "struct with nested map and struct",
testStruct: struct {
Key string
Map map[string]string
Str struct {
Inner string
}
}{
Key: "Value",
Map: map[string]string{
"key1": "value1",
"key2": "value2",
},
Str: struct {
Inner string
}{
Inner: "inner",
},
},
expectedLen: 4,
expectedEnvs: []string{"KEY=Value", "MAP_KEY1=value1", "MAP_KEY2=value2", "STR_INNER=inner"},
},
}

Expand All @@ -132,7 +155,8 @@ func TestParseEnvsValues(t *testing.T) {
envs := helper.ParseEnvs()

assert.Len(t, envs, tc.expectedLen)
assert.EqualValues(t, tc.expectedEnvs, envs)
// Compare arrays disregarding the order
assert.ElementsMatch(t, tc.expectedEnvs, envs)
})
}
}
Expand Down

0 comments on commit c440378

Please sign in to comment.