Added websocket hub for handling websocket clients, with a wsserve command
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
This commit is contained in:
1 parent
803a8929e6
commit
c9f42b62ad
42 files changed
+3619
-57
No files matched your search
+122
-15
@@ -1,13 +1,16 @@
|
||||
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.String: true, reflect.Int: true, reflect.Bool: true, reflect.Slice: true,
|
||||
}
|
||||
|
||||
// Every key round-trips through TypeOf/ValueOf/SetValue and has a type that
|
||||
@@ -39,19 +42,40 @@ 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. Sections are found by walking JobSearchConfig, so a new
|
||||
// section is checked without being listed anywhere.
|
||||
// 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) {
|
||||
var conf JobSearchConfig
|
||||
conf := defaultConfig(t)
|
||||
if !reflect.PointerTo(reflect.TypeOf(conf)).Implements(sectionType) {
|
||||
t.Error("*JobSearchConfig does not implement Section")
|
||||
}
|
||||
all := conf.Flatten()
|
||||
checkSections(t, all, "", reflect.ValueOf(&conf).Elem())
|
||||
uses := map[reflect.Type]int{}
|
||||
countSections(uses, reflect.TypeOf(conf))
|
||||
checkSections(t, &conf, all, uses, "", reflect.ValueOf(&conf).Elem())
|
||||
}
|
||||
|
||||
func checkSections(t *testing.T, all map[string]any, prefix string, section reflect.Value) {
|
||||
// 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
|
||||
}
|
||||
@@ -59,23 +83,106 @@ func checkSections(t *testing.T, all map[string]any, prefix string, section refl
|
||||
name := fv.Type().Name()
|
||||
if !fv.Addr().Type().Implements(sectionType) {
|
||||
t.Errorf("*%s does not implement Section", name)
|
||||
continue
|
||||
}
|
||||
checkEmbedded(t, fv.Type())
|
||||
if flatten := fv.MethodByName("Flatten"); flatten.IsValid() {
|
||||
keys := flatten.Call(nil)[0].Interface().(map[string]any)
|
||||
for k := range keys {
|
||||
if _, ok := all[joinKey(prefix, k)]; !ok {
|
||||
t.Errorf("%s.Flatten() key %q does not match its cfg tag %q",
|
||||
name, k, joinKey(prefix, f.key))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
checkSections(t, all, joinKey(prefix, f.key), fv)
|
||||
|
||||
// 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
|
||||
// JobSearchConfigSource, implement Section too.
|
||||
// JSCSourcesSource, implement Section too.
|
||||
func checkEmbedded(t *testing.T, section reflect.Type) {
|
||||
for i := 0; i < section.NumField(); i++ {
|
||||
sf := section.Field(i)
|
||||
|
||||
Reference in new issue
Block a user