package config import ( "fmt" "maps" "reflect" "strings" "testing" ) // Kinds that LoadFromViper knows how to read. var viperKinds = map[reflect.Kind]bool{ reflect.String: true, reflect.Int: true, reflect.Bool: true, reflect.Slice: true, } // Every key round-trips through TypeOf/ValueOf/SetValue and has a type that // LoadFromViper can read. func TestKeysRoundTrip(t *testing.T) { var conf JobSearchConfig if err := applyDefaults(&conf); err != nil { t.Fatalf("defaults: %v", err) } for key, want := range conf.Flatten() { kind, err := conf.TypeOf(key) if err != nil { t.Errorf("TypeOf(%q): %v", key, err) } else if !viperKinds[kind] { t.Errorf( "TypeOf(%q) = %v, which LoadFromViper does not handle", key, kind, ) } got, err := conf.ValueOf(key) if err != nil || !reflect.DeepEqual(got, want) { t.Errorf("ValueOf(%q) = %#v, %v; want %#v", key, got, err, want) } if err := conf.SetValue(key, got); err != nil { t.Errorf("SetValue(%q, %#v): %v", key, got, err) } } } // sectionType is the Section interface type. var sectionType = reflect.TypeOf((*Section)(nil)).Elem() // Every section, including embedded ones, implements Section, and its // Flatten keys (which use the section's JSCK* constant) line up with its cfg // tag in the parent. A section type used under more than one key anywhere in // the config, such as JSCRateLimitRule, has no constant, so its keys are // unprefixed. Each section's Map, TypeOf, ValueOf and SetValue agree with the // whole configuration's view of the same keys, and so do those of each item // in a list of sections. Sections are found by walking JobSearchConfig, so a // new section is checked without being listed anywhere. func TestSections(t *testing.T) { conf := defaultConfig(t) if !reflect.PointerTo(reflect.TypeOf(conf)).Implements(sectionType) { t.Error("*JobSearchConfig does not implement Section") } all := conf.Flatten() uses := map[reflect.Type]int{} countSections(uses, reflect.TypeOf(conf)) checkSections(t, &conf, all, uses, "", reflect.ValueOf(&conf).Elem()) } // countSections counts how many fields of each section type there are in the // tree under section. func countSections(uses map[reflect.Type]int, section reflect.Type) { for _, f := range fieldsOf(section) { if f.section { uses[f.fieldType]++ countSections(uses, f.fieldType) } } } func checkSections( t *testing.T, conf *JobSearchConfig, all map[string]any, uses map[reflect.Type]int, prefix string, section reflect.Value, ) { t.Helper() for _, f := range fieldsOf(section.Type()) { key := joinKey(prefix, f.key) if isSectionList(f.fieldType) { checkSectionList(t, all, key, section.FieldByIndex(f.index)) } if !f.section { continue } fv := section.FieldByIndex(f.index) name := fv.Type().Name() if !fv.Addr().Type().Implements(sectionType) { t.Errorf("*%s does not implement Section", name) continue } checkEmbedded(t, fv.Type()) keys := fv.Addr().Interface().(Section).Flatten() keyPrefix := prefix if uses[f.fieldType] > 1 { keyPrefix = key } for k := range keys { if _, ok := all[joinKey(keyPrefix, k)]; !ok { t.Errorf( "%s.Flatten() key %q does not match its cfg tag %q", name, k, key, ) } } // The section's own keys, relative to it, with the whole // configuration's values and kinds for them. want := map[string]any{} kinds := map[string]reflect.Kind{} for k, v := range all { if rel, ok := strings.CutPrefix(k, key+"."); ok { want[rel] = v kinds[rel], _ = conf.TypeOf(k) } } checkSectionMethods(t, name, fv, want, kinds) checkSections(t, conf, all, uses, key, fv) } } // checkSectionList checks the items of the list of sections at key implement // Section and that their methods agree with the list's value in all. func checkSectionList( t *testing.T, all map[string]any, key string, list reflect.Value, ) { t.Helper() name := list.Type().Elem().Name() if !reflect.PointerTo(list.Type().Elem()).Implements(sectionType) { t.Errorf("*%s (items of %s) does not implement Section", name, key) return } items, _ := all[key].([]map[string]any) if len(items) != list.Len() { t.Errorf("%s has %d items; Flatten gives %d", key, list.Len(), len(items)) return } for i := range list.Len() { item, itemName := list.Index(i), fmt.Sprintf("%s[%d]", key, i) want := flattenMap("", items[i]) // Items are not prefixed, and their kinds are their fields' kinds. got := item.Addr().Interface().(Section).Flatten() if !reflect.DeepEqual(got, want) { t.Errorf("%s.Flatten() =\n%v\nwant\n%v", itemName, got, want) } kinds := map[string]reflect.Kind{} for k := range want { _, f, _ := lookup(item, k) kinds[k] = kindOf(f.fieldType) } checkSectionMethods(t, itemName, item, want, kinds) } } // checkSectionMethods checks section's Map, TypeOf, ValueOf and SetValue // against want, its keys and values, and kinds, their kinds. // Setting each key to its own value must succeed and leave section unchanged. func checkSectionMethods( t *testing.T, name string, section reflect.Value, want map[string]any, kinds map[string]reflect.Kind, ) { t.Helper() s := section.Addr().Interface().(Section) if got := flattenMap("", s.Map()); !reflect.DeepEqual(got, want) { t.Errorf("%s.Map() =\n%v\nwant\n%v", name, got, want) } before := section.Interface() for k, v := range want { if kind, err := s.TypeOf(k); err != nil || kind != kinds[k] { t.Errorf("%s.TypeOf(%q) = %v, %v; want %v", name, k, kind, err, kinds[k]) } if got, err := s.ValueOf(k); err != nil || !reflect.DeepEqual(got, v) { t.Errorf("%s.ValueOf(%q) = %#v, %v; want %#v", name, k, got, err, v) } if err := s.SetValue(k, v); err != nil { t.Errorf("%s.SetValue(%q, %#v): %v", name, k, v, err) } } if !reflect.DeepEqual(section.Interface(), before) { t.Errorf("%s changed by setting its keys to their own values", name) } } // flattenMap flattens a map with nested maps, as Map gives, into dotted keys // prefixed with prefix (if non-empty). func flattenMap(prefix string, m map[string]any) map[string]any { flat := map[string]any{} for k, v := range m { if nested, ok := v.(map[string]any); ok { maps.Copy(flat, flattenMap(joinKey(prefix, k), nested)) } else { flat[joinKey(prefix, k)] = v } } return flat } // checkEmbedded checks that structs embedded in a section, such as // JSCSourcesSource, implement Section too. func checkEmbedded(t *testing.T, section reflect.Type) { t.Helper() for i := 0; i < section.NumField(); i++ { sf := section.Field(i) if !sf.Anonymous || sf.Type.Kind() != reflect.Struct { continue } if !reflect.PointerTo(sf.Type).Implements(sectionType) { t.Errorf( "*%s (embedded in %s) does not implement Section", sf.Type.Name(), section.Name(), ) } checkEmbedded(t, sf.Type) } }