From c7e42a754499558349f44e035d35132354c19041 Mon Sep 17 00:00:00 2001 From: Ryan D McGuire Date: Tue, 2 Sep 2025 13:06:02 -0400 Subject: [PATCH] updates Go dependencies, refactors and expands unit tests for config, logging, OpenTelemetry, HTTP, and gRPC components --- go.mod | 49 ++++++------ go.sum | 94 +++++++++++----------- pkg/app/setup_test.go | 123 ----------------------------- pkg/config/config_test.go | 102 ++++++++++++++++++++++++ pkg/config/testdata/config.yaml | 5 ++ pkg/logging/logging_test.go | 133 ++++++++++++++++++++++++++++++++ pkg/otel/otel_test.go | 66 ++++++++++++++++ pkg/srv/grpc/grpc_test.go | 111 ++++++++++++++++++++++++++ pkg/srv/http/http_test.go | 88 +++++++++++++++++++++ 9 files changed, 578 insertions(+), 193 deletions(-) delete mode 100644 pkg/app/setup_test.go create mode 100644 pkg/config/testdata/config.yaml create mode 100644 pkg/logging/logging_test.go create mode 100644 pkg/otel/otel_test.go create mode 100644 pkg/srv/grpc/grpc_test.go create mode 100644 pkg/srv/http/http_test.go diff --git a/go.mod b/go.mod index 1e7071e..54c7186 100644 --- a/go.mod +++ b/go.mod @@ -2,62 +2,63 @@ module gitea.libretechconsulting.com/rmcguire/go-app go 1.24.2 -replace github.com/prometheus/otlptranslator => github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f - require ( buf.build/go/protovalidate v0.14.0 github.com/caarlos0/env/v11 v11.3.1 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 github.com/prometheus/client_golang v1.23.0 github.com/rs/zerolog v1.34.0 + github.com/stretchr/testify v1.11.1 github.com/swaggest/jsonschema-go v0.3.78 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 - go.opentelemetry.io/otel/exporters/prometheus v0.59.1 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 - go.opentelemetry.io/otel/metric v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 - go.opentelemetry.io/otel/sdk/metric v1.37.0 - go.opentelemetry.io/otel/trace v1.37.0 - google.golang.org/grpc v1.74.2 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + google.golang.org/grpc v1.75.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 // indirect cel.dev/expr v0.24.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/cel-go v0.26.0 // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/swaggest/refl v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/go.sum b/go.sum index 6c55c35..73e1678 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1 h1:/AZH8sVB6LHv8G+hZlAMCP31NevnesHwYgnlgS5Vt14= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1/go.mod h1:eva/VCrd8X7xuJw+JtwCEyrCKiRRASukFqmirnWBvFU= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 h1:sjY1k5uszbIZfv11HO2keV4SLhNA47SabPO886v7Rvo= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1/go.mod h1:8EQ5GzyGJQ5tEIwMSxCl8RKJYsjCpAwkdcENoioXT6g= buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A= buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= @@ -32,8 +32,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -42,8 +42,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -72,8 +72,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f h1:QQB6SuvGZjK8kdc2YaLJpYhV8fxauOsjE6jgcL6YJ8Q= -github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -91,8 +91,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggest/jsonschema-go v0.3.78 h1:5+YFQrLxOR8z6CHvgtZc42WRy/Q9zRQQ4HoAxlinlHw= @@ -105,38 +105,38 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/prometheus v0.59.1 h1:HcpSkTkJbggT8bjYP+BjyqPWlD17BH9C5CYNKeDzmcA= -go.opentelemetry.io/otel/exporters/prometheus v0.59.1/go.mod h1:0FJL+gjuUoM07xzik3KPBaN+nz/CoB15kV6WLMiXZag= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -146,14 +146,16 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58= -google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g= +google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/app/setup_test.go b/pkg/app/setup_test.go deleted file mode 100644 index 4efaf6d..0000000 --- a/pkg/app/setup_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package app - -import ( - "bufio" - "context" - "flag" - "io" - "os" - "regexp" - "testing" - - "github.com/rs/zerolog" - - "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" -) - -func TestMustSetupConfigAndLogging(t *testing.T) { - // Configure app and logger - type inputs struct { - envs map[string]string - } - type outputs struct { - appName string - logLevel zerolog.Level - logRegexChecks []*regexp.Regexp - } - tests := []struct { - name string - inputs inputs - want outputs - }{ - { - name: "Test json logging with short timestamp", - inputs: inputs{ - envs: map[string]string{ - "APP_NAME": "testapp", - "APP_LOG_LEVEL": "warn", - "APP_LOG_FORMAT": "json", - "APP_LOG_TIME_FORMAT": "short", - }, - }, - want: outputs{ - appName: "testapp", - logLevel: zerolog.WarnLevel, - logRegexChecks: []*regexp.Regexp{ - regexp.MustCompile(`^\{.*time":"\d{1,}:\d{2}`), - }, - }, - }, - { - name: "Test json logging with unix timestamp", - inputs: inputs{ - envs: map[string]string{ - "APP_NAME": "testapp", - "APP_LOG_LEVEL": "info", - "APP_LOG_FORMAT": "json", - "APP_LOG_TIME_FORMAT": "unix", - }, - }, - want: outputs{ - appName: "testapp", - logLevel: zerolog.InfoLevel, - logRegexChecks: []*regexp.Regexp{ - regexp.MustCompile(`time":\d+,`), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set environment variables - for key, val := range tt.inputs.envs { - os.Setenv(key, val) - } - - // Prepare config in context - ctx := MustSetupConfigAndLogging(context.Background()) - - // Retrieve config and logger from prepared context - cfg := config.MustFromCtx(ctx) - logger := zerolog.Ctx(ctx) - - // Check wants - if cfg.Name != tt.want.appName { - t.Errorf("Expected app name %s, got %s", tt.want.appName, cfg.Name) - } - if logger.GetLevel() != tt.want.logLevel { - t.Errorf("Expected log level %#v, got %#v", tt.want.logLevel, logger.GetLevel()) - } - - // Send and capture a log - r, w := io.Pipe() - testLogger := logger.Output(w) - scanner := bufio.NewScanner(r) - - go func() { - testLogger.Error().Msg("test message") - w.Close() - }() - - logOut := make([]byte, 0) - if scanner.Scan() { - logOut = scanner.Bytes() - } - - // Check all expressions - for _, expr := range tt.want.logRegexChecks { - if !expr.Match(logOut) { - t.Errorf("Regex %s did not match log %s", expr.String(), logOut) - } - } - - // Super annoying need to reset due to app framework package - // using flag.Parse() and go test also using it - testlog := flag.Lookup("test.testlogfile").Value.String() - flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) - flag.String("test.testlogfile", testlog, "") - flag.String("test.paniconexit0", "", "") - flag.String("test.v", "", "") - flag.Parse() - }) - } -} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 16c88bb..1995ade 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "os" "testing" ) @@ -61,9 +62,110 @@ func Test_loadConfig(t *testing.T) { want: testDefaultConfig, wantErr: false, }, + { + name: "Load from file", + args: args{configPath: "./testdata/config.yaml"}, + want: &AppConfig{ + Name: "test-app", + Environment: "development", + Version: getVersion(), + Logging: &LogConfig{ + Enabled: true, + Level: "debug", + Format: LogFormatJSON, + Output: "stderr", + TimeFormat: TimeFormatLong, + }, + HTTP: &HTTPConfig{ + Enabled: true, + Listen: "127.0.0.1:9090", + LogRequests: false, + ReadTimeout: "10s", + WriteTimeout: "10s", + IdleTimeout: "1m", + }, + GRPC: &GRPCConfig{ + Enabled: false, + Listen: "127.0.0.1:8081", + LogRequests: false, + EnableReflection: true, + EnableInstrumentation: true, + }, + OTEL: &OTELConfig{ + Enabled: true, + PrometheusEnabled: true, + PrometheusPath: "/metrics", + StdoutEnabled: false, + MetricIntervalSecs: 30, + }, + }, + }, + { + name: "Load from env", + args: args{configPath: ""}, + want: &AppConfig{ + Name: "test-app-env", + Environment: "production", + Version: getVersion(), + Logging: &LogConfig{ + Enabled: false, + Level: "error", + Format: LogFormatConsole, + Output: "stdout", + TimeFormat: TimeFormatUnix, + }, + HTTP: &HTTPConfig{ + Enabled: false, + Listen: "0.0.0.0:80", + LogRequests: true, + ReadTimeout: "15s", + WriteTimeout: "15s", + IdleTimeout: "2m", + }, + GRPC: &GRPCConfig{ + Enabled: true, + Listen: "0.0.0.0:443", + LogRequests: true, + EnableReflection: false, + EnableInstrumentation: false, + }, + OTEL: &OTELConfig{ + Enabled: false, + PrometheusEnabled: false, + PrometheusPath: "/metricsz", + StdoutEnabled: true, + MetricIntervalSecs: 60, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.name == "Load from env" { + os.Setenv("APP_NAME", "test-app-env") + os.Setenv("APP_ENVIRONMENT", "production") + os.Setenv("APP_LOG_ENABLED", "false") + os.Setenv("APP_LOG_LEVEL", "error") + os.Setenv("APP_LOG_FORMAT", "console") + os.Setenv("APP_LOG_OUTPUT", "stdout") + os.Setenv("APP_LOG_TIME_FORMAT", "unix") + os.Setenv("APP_HTTP_ENABLED", "false") + os.Setenv("APP_HTTP_LISTEN", "0.0.0.0:80") + os.Setenv("APP_HTTP_LOG_REQUESTS", "true") + os.Setenv("APP_HTTP_READ_TIMEOUT", "15s") + os.Setenv("APP_HTTP_WRITE_TIMEOUT", "15s") + os.Setenv("APP_HTTP_IDLE_TIMEOUT", "2m") + os.Setenv("APP_GRPC_ENABLED", "true") + os.Setenv("APP_GRPC_LISTEN", "0.0.0.0:443") + os.Setenv("APP_GRPC_LOG_REQUESTS", "true") + os.Setenv("APP_GRPC_ENABLE_REFLECTION", "false") + os.Setenv("APP_GRPC_ENABLE_INSTRUMENTATION", "false") + os.Setenv("APP_OTEL_ENABLED", "false") + os.Setenv("APP_OTEL_PROMETHEUS_ENABLED", "false") + os.Setenv("APP_OTEL_PROMETHEUS_PATH", "/metricsz") + os.Setenv("APP_OTEL_STDOUT_ENABLED", "true") + os.Setenv("APP_OTEL_METRIC_INTERVAL_SECS", "60") + } got, err := loadConfig(tt.args.configPath) if (err != nil) != tt.wantErr { t.Errorf("loadConfig() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/config/testdata/config.yaml b/pkg/config/testdata/config.yaml new file mode 100644 index 0000000..f25070e --- /dev/null +++ b/pkg/config/testdata/config.yaml @@ -0,0 +1,5 @@ +name: test-app +logging: + level: debug +http: + listen: "127.0.0.1:9090" diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go new file mode 100644 index 0000000..9814194 --- /dev/null +++ b/pkg/logging/logging_test.go @@ -0,0 +1,133 @@ + +package logging + +import ( + "bytes" + "context" + "encoding/json" + "io" + "os" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" +) + +func TestMustInitLogging(t *testing.T) { + cfg := &config.AppConfig{ + Logging: &config.LogConfig{ + Level: "info", + Format: "json", + }, + } + ctx := cfg.AddToCtx(context.Background()) + + ctx = MustInitLogging(ctx) + logger := zerolog.Ctx(ctx) + + assert.NotNil(t, logger) + assert.Equal(t, zerolog.InfoLevel, logger.GetLevel()) +} + +func TestConfigureLogger(t *testing.T) { + type args struct { + cfg *config.LogConfig + } + tests := []struct { + name string + args args + level zerolog.Level + wantJSON bool + wantTime bool + assertion func(t *testing.T, output string) + }{ + { + name: "json logger", + args: args{ + cfg: &config.LogConfig{ + Level: "debug", + Format: "json", + }, + }, + level: zerolog.DebugLevel, + wantJSON: true, + }, + { + name: "console logger", + args: args{ + cfg: &config.LogConfig{ + Level: "warn", + Format: "console", + }, + }, + level: zerolog.WarnLevel, + wantJSON: false, + }, + { + name: "with time", + args: args{ + cfg: &config.LogConfig{ + Level: "info", + Format: "json", + TimeFormat: "unix", + }, + }, + level: zerolog.InfoLevel, + wantJSON: true, + wantTime: true, + assertion: func(t *testing.T, output string) { + var log map[string]interface{} + err := json.Unmarshal([]byte(output), &log) + assert.NoError(t, err) + assert.Contains(t, log, "time") + }, + }, + { + name: "without time", + args: args{ + cfg: &config.LogConfig{ + Level: "info", + Format: "json", + TimeFormat: "off", + }, + }, + level: zerolog.InfoLevel, + wantJSON: true, + wantTime: false, + assertion: func(t *testing.T, output string) { + var log map[string]interface{} + err := json.Unmarshal([]byte(output), &log) + assert.NoError(t, err) + assert.NotContains(t, log, "time") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Capture output + old := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + logger, err := configureLogger(tt.args.cfg) + assert.NoError(t, err) + assert.Equal(t, tt.level, logger.GetLevel()) + + logger.Info().Msg("test") + + w.Close() + os.Stderr = old + + var buf bytes.Buffer + io.Copy(&buf, r) + + output := buf.String() + + if tt.assertion != nil { + tt.assertion(t, output) + } + }) + } +} diff --git a/pkg/otel/otel_test.go b/pkg/otel/otel_test.go new file mode 100644 index 0000000..f2372d8 --- /dev/null +++ b/pkg/otel/otel_test.go @@ -0,0 +1,66 @@ + +package otel + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/trace" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" +) + +func TestInit(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + OTEL: &config.OTELConfig{ + Enabled: true, + }, + } + ctx := cfg.AddToCtx(context.Background()) + + ctx, shutdown := Init(ctx) + defer shutdown(context.Background()) + + assert.NotNil(t, ctx.Value(ctxKeyTracer)) + assert.NotNil(t, ctx.Value(ctxKeyMeter)) +} + +func TestInit_Disabled(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + OTEL: &config.OTELConfig{ + Enabled: false, + }, + } + ctx := cfg.AddToCtx(context.Background()) + + ctx, shutdown := Init(ctx) + defer shutdown(context.Background()) + + assert.NotNil(t, ctx.Value(ctxKeyTracer)) + assert.NotNil(t, ctx.Value(ctxKeyMeter)) +} + +func TestContextFuncs(t *testing.T) { + tracer := trace.NewNoopTracerProvider().Tracer("test") + meter := noop.NewMeterProvider().Meter("test") + + ctx := context.Background() + ctx = AddTracerToCtx(ctx, tracer) + ctx = AddMeterToCtx(ctx, meter) + + assert.Equal(t, tracer, MustTracerFromCtx(ctx)) + assert.Equal(t, meter, MustMeterFromCtx(ctx)) +} + +func TestContextFuncs_Panic(t *testing.T) { + assert.Panics(t, func() { + MustTracerFromCtx(context.Background()) + }) + assert.Panics(t, func() { + MustMeterFromCtx(context.Background()) + }) +} diff --git a/pkg/srv/grpc/grpc_test.go b/pkg/srv/grpc/grpc_test.go new file mode 100644 index 0000000..eca604c --- /dev/null +++ b/pkg/srv/grpc/grpc_test.go @@ -0,0 +1,111 @@ + +package grpc + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" +) + +// Mock gRPC service +type mockService struct{} + +type MockServiceServer interface { + TestMethod(context.Context, *mockRequest) (*mockResponse, error) +} + +type mockRequest struct{} +type mockResponse struct{} + +func (s *mockService) TestMethod(ctx context.Context, req *mockRequest) (*mockResponse, error) { + return &mockResponse{}, nil +} + +var _MockService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "test.MockService", + HandlerType: (*MockServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TestMethod", + Handler: func(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(mockRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MockServiceServer).TestMethod(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/test.MockService/TestMethod", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MockServiceServer).TestMethod(ctx, req.(*mockRequest)) + } + return interceptor(ctx, in, info, handler) + }, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "mock.proto", +} + +func TestInitGRPCServer(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + GRPC: &config.GRPCConfig{ + Enabled: true, + Listen: "127.0.0.1:0", // Use random available port + }, + } + ctx := cfg.AddToCtx(context.Background()) + ctx, _ = otel.Init(ctx) + + grpcOpts := &opts.GRPCOpts{ + GRPCConfig: cfg.GRPC, + AppGRPC: &opts.AppGRPC{ + Services: []*opts.GRPCService{ + { + Name: "mock", + Type: &_MockService_serviceDesc, + Service: &mockService{}, + }, + }, + }, + } + + shutdown, done, err := InitGRPCServer(ctx, grpcOpts) + assert.NoError(t, err) + assert.NotNil(t, shutdown) + assert.NotNil(t, done) + + // Shutdown the server + err = shutdown(context.Background()) + assert.NoError(t, err) +} + +func TestInitGRPCServer_NoServices(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + GRPC: &config.GRPCConfig{ + Enabled: true, + Listen: "127.0.0.1:0", + }, + } + ctx := cfg.AddToCtx(context.Background()) + ctx, _ = otel.Init(ctx) + + grpcOpts := &opts.GRPCOpts{ + GRPCConfig: cfg.GRPC, + AppGRPC: &opts.AppGRPC{}, + } + + _, _, err := InitGRPCServer(ctx, grpcOpts) + assert.Error(t, err) +} diff --git a/pkg/srv/http/http_test.go b/pkg/srv/http/http_test.go new file mode 100644 index 0000000..e30d75e --- /dev/null +++ b/pkg/srv/http/http_test.go @@ -0,0 +1,88 @@ + +package http + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" +) + +func TestInitHTTPServer(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + HTTP: &config.HTTPConfig{ + Enabled: true, + Listen: "127.0.0.1:0", // Use random available port + }, + OTEL: &config.OTELConfig{ + Enabled: true, + }, + } + ctx := cfg.AddToCtx(context.Background()) + ctx, _ = otel.Init(ctx) + + httpOpts := &opts.AppHTTP{ + Ctx: ctx, + Funcs: []opts.HTTPFunc{ + { + Path: "/test", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + }, + }, + } + + shutdown, done, err := InitHTTPServer(httpOpts) + assert.NoError(t, err) + assert.NotNil(t, shutdown) + assert.NotNil(t, done) + + // Shutdown the server + err = shutdown(context.Background()) + assert.NoError(t, err) +} + +func TestHealthCheck(t *testing.T) { + req := httptest.NewRequest("GET", "/health", nil) + w := httptest.NewRecorder() + + handleHealthCheckFunc(context.Background())(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "ok", string(body)) +} + +func TestLoggingMiddleware(t *testing.T) { + cfg := &config.AppConfig{ + Name: "test-app", + HTTP: &config.HTTPConfig{ + LogRequests: true, + }, + } + ctx := cfg.AddToCtx(context.Background()) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + middleware := loggingMiddleware(ctx, handler) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + + middleware.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +}