* feat: add code generation for Unifi client interface * chore: apply linter hints * chore: add tests * fix: nondeterministic client function signature due to iteration of map params * chore: add move version tests * chore: add more generator tests * chore: lint
372 lines
10 KiB
Go
372 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFieldInfoFromValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for i, c := range []struct {
|
|
expectedType string
|
|
expectedComment string
|
|
expectedOmitEmpty bool
|
|
validation interface{}
|
|
}{
|
|
{"string", "", true, ""},
|
|
{"string", "default|custom", true, "default|custom"},
|
|
{"string", ".{0,32}", true, ".{0,32}"},
|
|
{"string", "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$", false, "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$"},
|
|
|
|
{"int", "^([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$", true, "^([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$"},
|
|
{"int", "", true, "^[0-9]*$"},
|
|
|
|
{"float64", "", true, "[-+]?[0-9]*\\.?[0-9]+"},
|
|
// this one is really an error as the . is not escaped
|
|
{"float64", "", true, "^([-]?[\\d]+[.]?[\\d]*)$"},
|
|
{"float64", "", true, "^([\\d]+[.]?[\\d]*)$"},
|
|
|
|
{"bool", "", false, "false|true"},
|
|
{"bool", "", false, "true|false"},
|
|
} {
|
|
t.Run(fmt.Sprintf("%d %s %s", i, c.expectedType, c.validation), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
resource := &Resource{
|
|
StructName: "TestType",
|
|
Types: make(map[string]*FieldInfo),
|
|
FieldProcessor: func(name string, f *FieldInfo) error { return nil },
|
|
}
|
|
|
|
fieldInfo, err := resource.fieldInfoFromValidation("fieldName", c.validation)
|
|
// actualType, actualComment, actualOmitEmpty, err := fieldInfoFromValidation(c.validation)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fieldInfo.FieldType != c.expectedType {
|
|
t.Fatalf("expected type %q got %q", c.expectedType, fieldInfo.FieldType)
|
|
}
|
|
if fieldInfo.FieldValidationComment != c.expectedComment {
|
|
t.Fatalf("expected comment %q got %q", c.expectedComment, fieldInfo.FieldValidationComment)
|
|
}
|
|
if fieldInfo.OmitEmpty != c.expectedOmitEmpty {
|
|
t.Fatalf("expected omitempty %t got %t", c.expectedOmitEmpty, fieldInfo.OmitEmpty)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResourceTypes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testData := `
|
|
{
|
|
"note": ".{0,1024}",
|
|
"date": "^$|^(20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])Z?$",
|
|
"mac": "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$",
|
|
"number": "\\d+",
|
|
"boolean": "true|false",
|
|
"nested_type": {
|
|
"nested_field": "^$"
|
|
},
|
|
"nested_type_array": [{
|
|
"nested_field": "^$"
|
|
}]
|
|
}
|
|
`
|
|
expectedFields := map[string]*FieldInfo{
|
|
"Note": NewFieldInfo("Note", "note", "string", "validate:\"omitempty,gte=0,lte=1024\"", ".{0,1024}", true, false, ""),
|
|
"Date": NewFieldInfo("Date", "date", "string", "", "^$|^(20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])Z?$", false, false, ""),
|
|
"MAC": NewFieldInfo("MAC", "mac", "string", "validate:\"omitempty,mac\"", "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", true, false, ""),
|
|
"Number": NewFieldInfo("Number", "number", "int", "", "", true, false, "emptyStringInt"),
|
|
"Boolean": NewFieldInfo("Boolean", "boolean", "bool", "", "", false, false, ""),
|
|
"NestedType": {
|
|
FieldName: "NestedType",
|
|
JSONName: "nested_type",
|
|
FieldType: "StructNestedType",
|
|
FieldValidationComment: "",
|
|
OmitEmpty: true,
|
|
IsArray: false,
|
|
Fields: map[string]*FieldInfo{
|
|
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "", "^$", false, false, ""),
|
|
},
|
|
},
|
|
"NestedTypeArray": {
|
|
FieldName: "NestedTypeArray",
|
|
JSONName: "nested_type_array",
|
|
FieldType: "StructNestedTypeArray",
|
|
FieldValidationComment: "",
|
|
OmitEmpty: true,
|
|
IsArray: true,
|
|
Fields: map[string]*FieldInfo{
|
|
"NestedFieldModified": NewFieldInfo("NestedFieldModified", "nested_field", "string", "", "^$", false, false, ""),
|
|
},
|
|
},
|
|
}
|
|
|
|
expectedStruct := map[string]*FieldInfo{
|
|
"Struct": {
|
|
FieldName: "Struct",
|
|
JSONName: "path",
|
|
FieldType: "struct",
|
|
FieldValidationComment: "",
|
|
OmitEmpty: false,
|
|
IsArray: false,
|
|
Fields: map[string]*FieldInfo{
|
|
" ID": NewFieldInfo("ID", "_id", "string", "", "", true, false, ""),
|
|
" SiteID": NewFieldInfo("SiteID", "site_id", "string", "", "", true, false, ""),
|
|
" _Spacer": nil,
|
|
" Hidden": NewFieldInfo("Hidden", "attr_hidden", "bool", "", "", true, false, ""),
|
|
" HiddenID": NewFieldInfo("HiddenID", "attr_hidden_id", "string", "", "", true, false, ""),
|
|
" NoDelete": NewFieldInfo("NoDelete", "attr_no_delete", "bool", "", "", true, false, ""),
|
|
" NoEdit": NewFieldInfo("NoEdit", "attr_no_edit", "bool", "", "", true, false, ""),
|
|
" _Spacer": nil,
|
|
" _Spacer": nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
for k, v := range expectedFields {
|
|
expectedStruct["Struct"].Fields[k] = v
|
|
}
|
|
|
|
expectation := &Resource{
|
|
StructName: "Struct",
|
|
ResourcePath: "path",
|
|
|
|
Types: map[string]*FieldInfo{
|
|
"Struct": expectedStruct["Struct"],
|
|
"StructNestedType": expectedStruct["Struct"].Fields["NestedType"],
|
|
"StructNestedTypeArray": expectedStruct["Struct"].Fields["NestedTypeArray"],
|
|
},
|
|
|
|
FieldProcessor: func(name string, f *FieldInfo) error {
|
|
if name == "NestedField" {
|
|
f.FieldName = "NestedFieldModified"
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
t.Run("structural test", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
resource := NewResource("Struct", "path")
|
|
resource.FieldProcessor = expectation.FieldProcessor
|
|
|
|
err := resource.processJSON(([]byte)(testData))
|
|
|
|
require.NoError(t, err, "No error processing JSON")
|
|
assert.Equal(t, expectation.StructName, resource.StructName)
|
|
assert.Equal(t, expectation.ResourcePath, resource.ResourcePath)
|
|
assert.Equal(t, expectation.Types, resource.Types)
|
|
})
|
|
}
|
|
|
|
func TestNormalizeValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"\\d+", "09"},
|
|
{"[-+]?[0-9]*\\.?[0-9]+", "09.09"},
|
|
{"^([0-9]|[1-9][0-9]|25[0-5])$", "0919092505"},
|
|
{"^(([0-9]\\.[0-9]{2})\\.){3}([0-9]\\.[0-9])$", "09.09.09.09"},
|
|
{"[+-]?[0-9]*\\.?[0-9]+", "09.09"},
|
|
{"[-]?[\\d]+[.]?[\\d]*", "09.09"},
|
|
{"^$|^(20[0-9]{2}T([01][0-9]):[1-5]:[0-9])Z?$", "2009T0109:15:09Z"},
|
|
{"false|true", "falsetrue"},
|
|
{"true|false", "truefalse"},
|
|
{".{0,32}", "."},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
actual := normalizeValidation(tc.input)
|
|
assert.Equal(t, tc.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
var testReps = []replacement{
|
|
{"dhcpd", "DHCPD"},
|
|
{"ip", "IP"},
|
|
}
|
|
|
|
func TestCleanName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
reps []replacement
|
|
expected string
|
|
}{
|
|
{"field replacements basic", "dhcpd_enabled", testReps, "DHCPD_enabled"},
|
|
{"field replacements multiple", "dhcpd_ip_mac", testReps, "DHCPD_IP_mac"},
|
|
{"field replacements no match", "something_else", testReps, "something_else"},
|
|
{"empty string", "", fieldReps, ""},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
a := assert.New(t)
|
|
actual := cleanName(tc.input, tc.reps)
|
|
a.Equal(tc.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSetting(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
structName string
|
|
expected bool
|
|
}{
|
|
{"Setting", true},
|
|
{"SettingUsg", true},
|
|
{"SettingGlobalAp", true},
|
|
{"Settings", true},
|
|
{"Device", false},
|
|
{"Network", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.structName, func(t *testing.T) {
|
|
t.Parallel()
|
|
resource := &Resource{StructName: tc.structName}
|
|
assert.Equal(t, tc.expected, resource.IsSetting())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldInfoFromValidationErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
fieldName string
|
|
validation interface{}
|
|
errorContains string
|
|
}{
|
|
{
|
|
"invalid validation type",
|
|
"field",
|
|
123,
|
|
"unable to determine type from validation",
|
|
},
|
|
{
|
|
"empty array",
|
|
"field",
|
|
[]interface{}{},
|
|
"",
|
|
},
|
|
{
|
|
"array with multiple items",
|
|
"field",
|
|
[]interface{}{"item1", "item2"},
|
|
"unknown validation",
|
|
},
|
|
{
|
|
"invalid nested validation",
|
|
"field",
|
|
map[string]interface{}{
|
|
"nested": 123,
|
|
},
|
|
"unable to determine type from validation",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
a := assert.New(t)
|
|
resource := NewResource("Test", "test")
|
|
fieldInfo, err := resource.fieldInfoFromValidation(tc.fieldName, tc.validation)
|
|
if tc.errorContains != "" {
|
|
require.ErrorContains(t, err, tc.errorContains)
|
|
a.NotNil(fieldInfo)
|
|
a.EqualValues(&FieldInfo{}, fieldInfo)
|
|
} else {
|
|
require.NoError(t, err)
|
|
a.NotNil(fieldInfo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildResourcesFromDownloadedFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a temporary directory for test files
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create test JSON files
|
|
validJSON := `{
|
|
"name": "test",
|
|
"value": "^[0-9]*$",
|
|
"enabled": "true|false"
|
|
}`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "Test.json"), []byte(validJSON), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(tmpDir, "Invalid.json"), []byte("invalid json"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(tmpDir, "Setting.json"), []byte(validJSON), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
// Test cases
|
|
tests := []struct {
|
|
name string
|
|
dir string
|
|
expectedLen int
|
|
errorContains string
|
|
}{
|
|
{
|
|
"valid directory",
|
|
tmpDir,
|
|
1, // Only Test.json should be processed (Setting.json is skipped, Invalid.json fails)
|
|
"",
|
|
},
|
|
{
|
|
"non-existent directory",
|
|
"non-existent",
|
|
0,
|
|
"unable to read fields directory",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
a := assert.New(t)
|
|
resources, err := buildResourcesFromDownloadedFields(tc.dir)
|
|
if tc.errorContains != "" {
|
|
require.ErrorContains(t, err, tc.errorContains)
|
|
a.Nil(resources)
|
|
} else {
|
|
require.NoError(t, err)
|
|
a.Len(resources, tc.expectedLen)
|
|
if tc.expectedLen > 0 {
|
|
a.Equal("Test", resources[0].StructName)
|
|
a.Equal("test", resources[0].ResourcePath)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|