which runs a plain TCP server for receiving websocket hub clients, with server config, CSRF and rate limit middleware. - internal/websocket: Hub pushing messages to WebSocket clients (ServeHTTP, example.com/ws upgrade with optional CSRF check) and plain TCP clients (ServeTCP, newline-delimited JSON envelopes); ping/pong keepalive, per-client message rate limit, slow consumer handling, graceful Shutdown, debug logging of messages sent and received - internal/websocket/tcpserver: standalone TCP server for a hub, with accept back-off, a per-IP connection limit (server.websocket.connection_limit) and graceful shutdown - internal/cli/wsserve: `wsserve` command running the TCP server, with --listen/--host/--port overrides - internal/middleware: keyed token bucket rate limiter, real client IP from trusted proxies - internal/httpx: RFC 9457 problem responses, request IDs in context - internal/config: server section (host, port, shutdown timeout, rate limiter idle TTL, websocket); util.ByteSize settings (e.g. "32KiB"); lists of sections as single keys; empty lists stored as nil; isExplicitlySet for printLoadedConfig; JobSearchConfigSource renamed to JSCSourcesSource; TestSections checks every section's methods - internal/util: ByteSize, parsed and formatted with go-humanize
199 lines
6.6 KiB
Go
199 lines
6.6 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) {
|
|
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) {
|
|
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)
|
|
}
|
|
}
|