Files
job-search/internal/config/config_test.go
T
bwilliams 465358609a Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names
Short and single-letter names in application code become full words (tests
keep theirs). Wrapped calls, signatures and composite literals put one item
per line, and one-line function bodies are expanded. Exported names and
groups of constants get doc comments, and comments refer to the renamed
identifiers.
2026-10-15 16:12:56 +01:00

221 lines
6.7 KiB
Go

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.typ]++
countSections(uses, f.typ)
}
}
}
func checkSections(
t *testing.T,
conf *JobSearchConfig,
all map[string]any,
uses map[reflect.Type]int,
prefix string,
section reflect.Value,
) {
for _, f := range fieldsOf(section.Type()) {
key := joinKey(prefix, f.key)
if isSectionList(f.typ) {
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.typ] > 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.
if got := item.Addr().Interface().(Section).Flatten(); !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.typ)
}
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,
) {
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)
}
}