diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 19896109b..eaeb752ee 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,20 @@ +--- version: 2 updates: - - package-ecosystem: "gomod" - directory: "/" + - package-ecosystem: "gomod" + open-pull-requests-limit: 10 # avoid spam, if no one reacts + directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "github-actions" + open-pull-requests-limit: 10 # avoid spam, if no one reacts + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + groups: + actions: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 704a95c5a..5be282e02 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,3 +1,4 @@ +--- name: Workflow-Pipeline on: @@ -61,7 +62,7 @@ jobs: fetch-depth: 0 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: Go @@ -82,7 +83,7 @@ jobs: - name: Upload Test Coverage if: ${{ matrix.go-version == '1.22'}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Example-Test-Report path: profile.cov @@ -101,7 +102,7 @@ jobs: fetch-depth: 0 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: Go @@ -119,7 +120,7 @@ jobs: - name: Upload Test Coverage if: ${{ matrix.go-version == '1.22'}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PKG-Coverage-Report path: profile.cov @@ -133,7 +134,7 @@ jobs: uses: actions/checkout@v4 - name: Download Coverage Report - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts @@ -167,7 +168,7 @@ jobs: uses: actions/checkout@v4 - name: Download Coverage Report - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts @@ -178,7 +179,7 @@ jobs: tail -n +2 ./PKG-Coverage-Report/profile.cov >> merged_profile.cov - name: Upload - uses: paambaati/codeclimate-action@v5.0.0 + uses: paambaati/codeclimate-action@v8.0.0 env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: @@ -188,7 +189,7 @@ jobs: code_quality: name: Code Quality🎖️ runs-on: ubuntu-latest - container: "golangci/golangci-lint:v1.57.2" + container: "golangci/golangci-lint:v1.59.1" steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 @@ -197,3 +198,14 @@ jobs: - name: GolangCI-Lint run: | golangci-lint run --timeout 9m0s + + linting_party: + name: Linting Party🥳 + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Check for file names errors + uses: ls-lint/action@v2.2.3 + with: + config: .ls-lint.yml \ No newline at end of file diff --git a/.github/workflows/website_prod.yml b/.github/workflows/website_prod.yml index a0850d865..12abf53b8 100644 --- a/.github/workflows/website_prod.yml +++ b/.github/workflows/website_prod.yml @@ -24,10 +24,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Log in to the GitHub Container registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ${{ env.WEBSITE_REGISTRY }} username: ${{ github.actor }} @@ -38,7 +38,7 @@ jobs: docker pull ${{ env.WEBSITE_REGISTRY }}/gofr-dev/website:latest - name: Build Image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: push: false context: ./ @@ -56,17 +56,17 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GAR - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: us-central1-docker.pkg.dev username: _json_key password: ${{ secrets.deploy_key }} - name: Log in to the GitHub Container registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ${{ env.WEBSITE_REGISTRY }} username: ${{ github.actor }} @@ -77,7 +77,7 @@ jobs: docker pull ${{ env.WEBSITE_REGISTRY }}/gofr-dev/website:latest - name: Build and Push Image Stage - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: push: true context: ./ @@ -102,7 +102,7 @@ jobs: uses: actions/checkout@v4 - name: Authorize to GCP service account - uses: google-github-actions/auth@v1 + uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.deploy_key }} diff --git a/.golangci.yml b/.golangci.yml index 64166f42b..8dcfe1ab0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,4 @@ +--- linters-settings: dupl: threshold: 100 @@ -8,7 +9,9 @@ linters-settings: statements: 50 gci: sections: - - prefix(gofr.dev) + - standard + - default + - localmodule goconst: min-len: 2 min-occurrences: 2 @@ -31,7 +34,7 @@ linters-settings: local-prefixes: github.com/golangci/golangci-lint golint: min-confidence: 0 - gomnd: + mnd: checks: - argument - case @@ -55,6 +58,36 @@ linters-settings: allow-unused: false # report any unused nolint directives require-explanation: true # require an explanation for nolint directives require-specific: true # require nolint directives to be specific about which linter is being skipped + revive: + rules: + # default revive rules, they have to be present otherwise they are disabled + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: empty-block + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: increment-decrement + - name: indent-error-flow + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: superfluous-else + - name: time-naming + - name: unexported-return + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: var-naming + # additional revive rules + - name: bare-return + - name: bool-literal-in-expr + - name: comment-spacings + - name: early-return + - name: defer linters: # please, do not use `enable-all`: it's deprecated and will be removed soon. @@ -65,7 +98,9 @@ linters: - bodyclose - dogsled - dupl + - err113 - errcheck + - errorlint - exhaustive - exportloopref - funlen @@ -76,17 +111,17 @@ linters: - gocritic - gocyclo - godot - - goerr113 - gofmt - goimports - - gomnd - goprintffuncname - gosec - gosimple - govet - ineffassign - lll + - mirror - misspell + - mnd - nakedret - nestif - noctx @@ -96,9 +131,11 @@ linters: - rowserrcheck - staticcheck - stylecheck + - thelper - unconvert - unparam - unused + - usestdlibvars - whitespace - wsl @@ -108,7 +145,7 @@ linters: service: - golangci-lint-version: 1.57.x + golangci-lint-version: 1.59.x issues: # exclude-use-default: false diff --git a/.ls-lint.yml b/.ls-lint.yml new file mode 100644 index 000000000..9890a7436 --- /dev/null +++ b/.ls-lint.yml @@ -0,0 +1,4 @@ +--- +ls: + .go: snake_case + .pb.go: snake_case diff --git a/docs/references/configs/page.md b/docs/references/configs/page.md index 4f0f12175..7c0c359b0 100644 --- a/docs/references/configs/page.md +++ b/docs/references/configs/page.md @@ -300,4 +300,9 @@ This document lists all the configuration options supported by the GoFr framewor - MQTT_QOS - Quality of Service Level +--- + +- MQTT_KEEP_ALIVE +- Sends regular messages to check the link is active. May not work as expected if handling func is blocking execution + {% /table %} diff --git a/examples/grpc-server/grpc/hello.pb.go b/examples/grpc-server/grpc/hello.pb.go index bf0743cee..a9c00225f 100644 --- a/examples/grpc-server/grpc/hello.pb.go +++ b/examples/grpc-server/grpc/hello.pb.go @@ -7,10 +7,11 @@ package grpc import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/examples/grpc-server/grpc/hello_grpc.pb.go b/examples/grpc-server/grpc/hello_grpc.pb.go index 9b144e915..83c78fa20 100644 --- a/examples/grpc-server/grpc/hello_grpc.pb.go +++ b/examples/grpc-server/grpc/hello_grpc.pb.go @@ -8,6 +8,7 @@ package grpc import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go index a681803b5..1b427d51f 100644 --- a/examples/http-server/main_test.go +++ b/examples/http-server/main_test.go @@ -38,7 +38,7 @@ func TestIntegration_SimpleAPIServer(t *testing.T) { } for i, tc := range tests { - req, _ := http.NewRequest("GET", host+tc.path, nil) + req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil) req.Header.Set("content-type", "application/json") c := http.Client{} @@ -92,7 +92,7 @@ func TestIntegration_SimpleAPIServer_Errors(t *testing.T) { } for i, tc := range tests { - req, _ := http.NewRequest("GET", host+tc.path, nil) + req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil) req.Header.Set("content-type", "application/json") c := http.Client{} @@ -129,7 +129,7 @@ func TestIntegration_SimpleAPIServer_Health(t *testing.T) { } for i, tc := range tests { - req, _ := http.NewRequest("GET", host+tc.path, nil) + req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil) req.Header.Set("content-type", "application/json") c := http.Client{} diff --git a/examples/using-custom-metrics/main_test.go b/examples/using-custom-metrics/main_test.go index 672615828..ea53fffde 100644 --- a/examples/using-custom-metrics/main_test.go +++ b/examples/using-custom-metrics/main_test.go @@ -16,7 +16,7 @@ func TestIntegration(t *testing.T) { c := http.Client{} - req, _ := http.NewRequest("POST", host+"/transaction", nil) + req, _ := http.NewRequest(http.MethodPost, host+"/transaction", nil) req.Header.Set("content-type", "application/json") _, err := c.Do(req) @@ -24,14 +24,14 @@ func TestIntegration(t *testing.T) { t.Fatalf("request to /transaction failed %v", err) } - req, _ = http.NewRequest("POST", host+"/return", nil) + req, _ = http.NewRequest(http.MethodPost, host+"/return", nil) _, err = c.Do(req) if err != nil { t.Fatalf("request to /transaction failed %v", err) } - req, _ = http.NewRequest("GET", "http://localhost:2120/metrics", nil) + req, _ = http.NewRequest(http.MethodGet, "http://localhost:2120/metrics", nil) resp, err := c.Do(req) if err != nil { diff --git a/examples/using-file-bind/main_test.go b/examples/using-file-bind/main_test.go index 6ef70a23c..25413dc1a 100644 --- a/examples/using-file-bind/main_test.go +++ b/examples/using-file-bind/main_test.go @@ -2,13 +2,14 @@ package main import ( "bytes" - "github.com/stretchr/testify/assert" "io" "mime/multipart" "net/http" "os" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestMain_BindError(t *testing.T) { diff --git a/examples/using-migrations/main.go b/examples/using-migrations/main.go index c8f0161b1..74f7fbabf 100644 --- a/examples/using-migrations/main.go +++ b/examples/using-migrations/main.go @@ -5,7 +5,6 @@ import ( "fmt" "gofr.dev/examples/using-migrations/migrations" - "gofr.dev/pkg/gofr" ) diff --git a/examples/using-migrations/migrations/1708322067_create_employee_table_test.go b/examples/using-migrations/migrations/1708322067_create_employee_table_test.go index 2aae7505f..9a4473c49 100644 --- a/examples/using-migrations/migrations/1708322067_create_employee_table_test.go +++ b/examples/using-migrations/migrations/1708322067_create_employee_table_test.go @@ -6,6 +6,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/assert" + "gofr.dev/pkg/gofr/migration" ) diff --git a/examples/using-migrations/migrations/1708322089_redis_add_employee_name.go b/examples/using-migrations/migrations/1708322089_redis_add_employee_name.go index dc28e8db4..6d16468a4 100644 --- a/examples/using-migrations/migrations/1708322089_redis_add_employee_name.go +++ b/examples/using-migrations/migrations/1708322089_redis_add_employee_name.go @@ -2,6 +2,7 @@ package migrations import ( "context" + "gofr.dev/pkg/gofr/migration" ) diff --git a/examples/using-migrations/migrations/1708322089_redis_add_employee_name_test.go b/examples/using-migrations/migrations/1708322089_redis_add_employee_name_test.go index 6d1eda832..3390107d6 100644 --- a/examples/using-migrations/migrations/1708322089_redis_add_employee_name_test.go +++ b/examples/using-migrations/migrations/1708322089_redis_add_employee_name_test.go @@ -6,6 +6,7 @@ import ( "github.com/go-redis/redismock/v9" "github.com/stretchr/testify/assert" + "gofr.dev/pkg/gofr/migration" ) diff --git a/examples/using-publisher/main.go b/examples/using-publisher/main.go index ac0c8107f..1fb22cfb9 100644 --- a/examples/using-publisher/main.go +++ b/examples/using-publisher/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "gofr.dev/pkg/gofr" ) diff --git a/examples/using-subscriber/main_test.go b/examples/using-subscriber/main_test.go index 46d2df584..97ee1f965 100644 --- a/examples/using-subscriber/main_test.go +++ b/examples/using-subscriber/main_test.go @@ -1,18 +1,50 @@ package main import ( + "context" "strings" "testing" "time" + "gofr.dev/pkg/gofr/datasource/pubsub/kafka" + "gofr.dev/pkg/gofr/logging" "gofr.dev/pkg/gofr/testutil" ) +type mockMetrics struct { +} + +func (m *mockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) { +} + +func initializeTest(t *testing.T) { + c := kafka.New(kafka.Config{ + Broker: "localhost:9092", + OffSet: 1, + BatchSize: kafka.DefaultBatchSize, + BatchBytes: kafka.DefaultBatchBytes, + BatchTimeout: kafka.DefaultBatchTimeout, + Partition: 1, + }, logging.NewMockLogger(logging.INFO), &mockMetrics{}) + + err := c.Publish(context.Background(), "order-logs", []byte(`{"data":{"orderId":"123","status":"pending"}}`)) + if err != nil { + t.Errorf("Error while publishing: %v", err) + } + + err = c.Publish(context.Background(), "products", []byte(`{"data":{"productId":"123","price":"599"}}`)) + if err != nil { + t.Errorf("Error while publishing: %v", err) + } +} + func TestExampleSubscriber(t *testing.T) { + initializeTest(t) + log := testutil.StdoutOutputForFunc(func() { const host = "http://localhost:8200" go main() - time.Sleep(time.Minute * 2) + time.Sleep(time.Second * 40) }) testCases := []struct { diff --git a/go.mod b/go.mod index de007ad2f..df9c6a5f8 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module gofr.dev go 1.22 require ( - cloud.google.com/go/pubsub v1.39.0 + cloud.google.com/go/pubsub v1.40.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/XSAM/otelsql v0.31.0 + github.com/XSAM/otelsql v0.32.0 github.com/alicebob/miniredis/v2 v2.33.0 github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/go-redis/redismock/v9 v9.2.0 @@ -26,27 +26,27 @@ require ( github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.52.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 - go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 - go.opentelemetry.io/otel/exporters/prometheus v0.49.0 - go.opentelemetry.io/otel/exporters/zipkin v1.27.0 - go.opentelemetry.io/otel/metric v1.27.0 - go.opentelemetry.io/otel/sdk v1.27.0 - go.opentelemetry.io/otel/sdk/metric v1.27.0 - go.opentelemetry.io/otel/trace v1.27.0 + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 + go.opentelemetry.io/otel/exporters/zipkin v1.28.0 + go.opentelemetry.io/otel/metric v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/sdk/metric v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/mock v0.4.0 golang.org/x/oauth2 v0.21.0 - golang.org/x/term v0.21.0 + golang.org/x/term v0.22.0 golang.org/x/text v0.16.0 - google.golang.org/api v0.186.0 - google.golang.org/grpc v1.64.0 + google.golang.org/api v0.187.0 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 modernc.org/sqlite v1.30.1 ) require ( cloud.google.com/go v0.115.0 // indirect - cloud.google.com/go/auth v0.6.0 // indirect + cloud.google.com/go/auth v0.6.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.8 // indirect @@ -59,7 +59,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -71,13 +71,14 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.53.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -90,11 +91,11 @@ require ( golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.52.1 // indirect diff --git a/go.sum b/go.sum index 548f2db53..eb796227f 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,27 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= -cloud.google.com/go/auth v0.6.0 h1:5x+d6b5zdezZ7gmLWD1m/xNjnaQ2YDhmIz/HH3doy1g= -cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g= +cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38= +cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/kms v1.17.1 h1:5k0wXqkxL+YcXd4viQzTqCgzzVKKxzgrK+rCZJytEQs= -cloud.google.com/go/kms v1.17.1/go.mod h1:DCMnCF/apA6fZk5Cj4XsD979OyHAqFasPuA5Sd0kGlQ= +cloud.google.com/go/kms v1.18.0 h1:pqNdaVmZJFP+i8OVLocjfpdTWETTYa20FWOegSCdrRo= +cloud.google.com/go/kms v1.18.0/go.mod h1:DyRBeWD/pYBMeyiaXFa/DGNyxMDL3TslIKb8o/JkLkw= cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/pubsub v1.39.0 h1:qt1+S6H+wwW8Q/YvDwM8lJnq+iIFgFEgaD/7h3lMsAI= -cloud.google.com/go/pubsub v1.39.0/go.mod h1:FrEnrSGU6L0Kh3iBaAbIUM8KMR7LqyEkMboVxGXCT+s= +cloud.google.com/go/pubsub v1.40.0 h1:0LdP+zj5XaPAGtWr2V6r88VXJlmtaB/+fde1q3TU8M0= +cloud.google.com/go/pubsub v1.40.0/go.mod h1:BVJI4sI2FyXp36KFKvFwcfDRDfR8MiLT8mMhmIhdAeA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/XSAM/otelsql v0.31.0 h1:AcWI+/BW4ANKyAybZmU9g9kjjSIcDEOFw96ybyM4cDo= -github.com/XSAM/otelsql v0.31.0/go.mod h1:iCkLyB/me+QC4yjymXjLimJiX0oklymiKeGxeGDTW24= +github.com/XSAM/otelsql v0.32.0 h1:vDRE4nole0iOOlTaC/Bn6ti7VowzgxK39n3Ll1Kt7i0= +github.com/XSAM/otelsql v0.32.0/go.mod h1:Ary0hlyVBbaSwo8atZB8Aoothg9s/LBJj/N/p5qDmLM= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= @@ -63,8 +63,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= @@ -142,6 +142,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -166,10 +168,10 @@ github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJL github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= -github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= @@ -220,24 +222,24 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0. go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.52.0/go.mod h1:l/UzmhdRx9YP37NI/nSr7l1bgG0dZnGfZf6C7TiV4jI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= -go.opentelemetry.io/otel/exporters/zipkin v1.27.0 h1:aXcxb7F6ZDC1o2Z52LDfS2g6M2FB5CrxdR2gzY4QRNs= -go.opentelemetry.io/otel/exporters/zipkin v1.27.0/go.mod h1:+WMURoi4KmVB7ypbFPx3xtZTWen2Ca3lRK9u6DVTO5M= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= -go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= +go.opentelemetry.io/otel/exporters/zipkin v1.28.0 h1:q86SrM4sgdc1eDABeA+307DUWy1qaT3fDCVbeKYGfY4= +go.opentelemetry.io/otel/exporters/zipkin v1.28.0/go.mod h1:mkxt8tmE/1YujUHsMIgTPvBN2HVE3kXlRZWeKsTsFgI= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -308,15 +310,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -345,28 +347,28 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug= -google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc= +google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= +google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 h1:CUiCqkPw1nNrNQzCCG4WA65m0nAmQiwXHpub3dNyruU= -google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4/go.mod h1:EvuUDCulqGgV80RvP1BHuom+smhX4qtlhnNatHuroGQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= -google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/gofr/cmd/request_test.go b/pkg/gofr/cmd/request_test.go index eb59cdc15..3cb6fd527 100644 --- a/pkg/gofr/cmd/request_test.go +++ b/pkg/gofr/cmd/request_test.go @@ -29,7 +29,7 @@ func TestRequest_Bind(t *testing.T) { _ = r.Bind(&a) - if a.Name != "gofr" || a.Valid != true || a.Value != 12 { + if a.Name != "gofr" || !a.Valid || a.Value != 12 { t.Errorf("TEST Failed.\nGot: %v\n%s", a, "Request Bind error") } diff --git a/pkg/gofr/cmd/responder_test.go b/pkg/gofr/cmd/responder_test.go index e8787d7b9..0eab84b57 100644 --- a/pkg/gofr/cmd/responder_test.go +++ b/pkg/gofr/cmd/responder_test.go @@ -17,7 +17,7 @@ func TestResponder_Respond(t *testing.T) { }) err := testutil.StderrOutputForFunc(func() { - r.Respond(nil, errors.New("error")) //nolint:goerr113 // We are testing if a dynamic error would work. + r.Respond(nil, errors.New("error")) //nolint:err113 // We are testing if a dynamic error would work. }) assert.Equal(t, "data\n", out, "TEST Failed.\n", "Responder stdout output") diff --git a/pkg/gofr/config/godotenv_test.go b/pkg/gofr/config/godotenv_test.go index 58f79e7c9..9d42b853f 100644 --- a/pkg/gofr/config/godotenv_test.go +++ b/pkg/gofr/config/godotenv_test.go @@ -116,6 +116,8 @@ func Test_EnvFailureWithHyphen(t *testing.T) { } func createEnvFile(t *testing.T, fileName string, envData map[string]string) { + t.Helper() + // Create or open the .env file for writing envFile, err := os.Create("configs/" + fileName) if err != nil { diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index 0fa27c2a3..056ce47cf 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -3,8 +3,10 @@ package container import ( "strconv" "strings" + "time" _ "github.com/go-sql-driver/mysql" // This is required to be blank import + "gofr.dev/pkg/gofr/config" "gofr.dev/pkg/gofr/datasource" "gofr.dev/pkg/gofr/datasource/file" @@ -113,35 +115,47 @@ func (c *Container) Create(conf config.Config) { SubscriptionName: conf.Get("GOOGLE_SUBSCRIPTION_NAME"), }, c.Logger, c.metricsManager) case "MQTT": - var qos byte - - port, _ := strconv.Atoi(conf.Get("MQTT_PORT")) - order, _ := strconv.ParseBool(conf.GetOrDefault("MQTT_MESSAGE_ORDER", "false")) - - switch conf.Get("MQTT_QOS") { - case "1": - qos = 1 - case "2": - qos = 2 - default: - qos = 0 - } + c.PubSub = c.createMqttPubSub(conf) + } - configs := &mqtt.Config{ - Protocol: conf.GetOrDefault("MQTT_PROTOCOL", "tcp"), // using tcp as default method to connect to broker - Hostname: conf.Get("MQTT_HOST"), - Port: port, - Username: conf.Get("MQTT_USER"), - Password: conf.Get("MQTT_PASSWORD"), - ClientID: conf.Get("MQTT_CLIENT_ID_SUFFIX"), - QoS: qos, - Order: order, - } + c.File = file.New(c.Logger) +} + +func (c *Container) createMqttPubSub(conf config.Config) pubsub.Client { + var qos byte - c.PubSub = mqtt.New(configs, c.Logger, c.metricsManager) + port, _ := strconv.Atoi(conf.Get("MQTT_PORT")) + order, _ := strconv.ParseBool(conf.GetOrDefault("MQTT_MESSAGE_ORDER", "false")) + + keepAlive, err := time.ParseDuration(conf.Get("MQTT_KEEP_ALIVE")) + if err != nil { + keepAlive = 30 * time.Second + + c.Logger.Debug("MQTT_KEEP_ALIVE is not set or ivalid, setting it to 30 seconds") } - c.File = file.New(c.Logger) + switch conf.Get("MQTT_QOS") { + case "1": + qos = 1 + case "2": + qos = 2 + default: + qos = 0 + } + + configs := &mqtt.Config{ + Protocol: conf.GetOrDefault("MQTT_PROTOCOL", "tcp"), // using tcp as default method to connect to broker + Hostname: conf.Get("MQTT_HOST"), + Port: port, + Username: conf.Get("MQTT_USER"), + Password: conf.Get("MQTT_PASSWORD"), + ClientID: conf.Get("MQTT_CLIENT_ID_SUFFIX"), + QoS: qos, + Order: order, + KeepAlive: keepAlive, + } + + return mqtt.New(configs, c.Logger, c.metricsManager) } // GetHTTPService returns registered HTTP services. diff --git a/pkg/gofr/container/datasources.go b/pkg/gofr/container/datasources.go index 5338f4ecd..d1bed6525 100644 --- a/pkg/gofr/container/datasources.go +++ b/pkg/gofr/container/datasources.go @@ -89,6 +89,8 @@ type Cassandra interface { // u := user{} // applied, err := c.ExecCAS(&ids, "INSERT INTO users VALUES(1, 'John Doe') IF NOT EXISTS") ExecCAS(dest interface{}, stmt string, values ...interface{}) (bool, error) + + HealthChecker } type CassandraProvider interface { @@ -101,6 +103,8 @@ type Clickhouse interface { Exec(ctx context.Context, query string, args ...any) error Select(ctx context.Context, dest any, query string, args ...any) error AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error + + HealthChecker } type ClickhouseProvider interface { @@ -154,6 +158,21 @@ type Mongo interface { // Drop an entire collection from the database. // It returns an error if any. Drop(ctx context.Context, collection string) error + + // CreateCollection creates a new collection with specified name and default options. + CreateCollection(ctx context.Context, name string) error + + // StartSession starts a session and provide methods to run commands in a transaction. + StartSession() (interface{}, error) + + HealthChecker +} + +type Transaction interface { + StartTransaction() error + AbortTransaction(context.Context) error + CommitTransaction(context.Context) error + EndSession(context.Context) } // MongoProvider is an interface that extends Mongo with additional methods for logging, metrics, and connection management. @@ -174,3 +193,9 @@ type provider interface { // Connect establishes a connection to Cassandra and registers metrics using the provided configuration when the client was Created. Connect() } + +type HealthChecker interface { + // HealthCheck returns an interface rather than a struct as externalDB's are part of different module. + // It is done to avoid adding packages which are not being used. + HealthCheck(context.Context) (any, error) +} diff --git a/pkg/gofr/container/health.go b/pkg/gofr/container/health.go index f21f48b6b..d65cd9e41 100644 --- a/pkg/gofr/container/health.go +++ b/pkg/gofr/container/health.go @@ -40,6 +40,8 @@ func (c *Container) Health(ctx context.Context) interface{} { healthMap["pubsub"] = health } + downCount += checkExternalDBHealth(ctx, c, healthMap) + for name, svc := range c.Services { health := svc.HealthCheck(ctx) if health.Status == statusDown { @@ -54,6 +56,37 @@ func (c *Container) Health(ctx context.Context) interface{} { return healthMap } +func checkExternalDBHealth(ctx context.Context, c *Container, healthMap map[string]interface{}) (downCount int) { + if !isNil(c.Mongo) { + health, err := c.Mongo.HealthCheck(ctx) + if err != nil { + downCount++ + } + + healthMap["mongo"] = health + } + + if !isNil(c.Cassandra) { + health, err := c.Cassandra.HealthCheck(ctx) + if err != nil { + downCount++ + } + + healthMap["cassandra"] = health + } + + if !isNil(c.Clickhouse) { + health, err := c.Clickhouse.HealthCheck(ctx) + if err != nil { + downCount++ + } + + healthMap["clickHouse"] = health + } + + return downCount +} + func (c *Container) appHealth(healthMap map[string]interface{}, downCount int) { healthMap["name"] = c.GetAppName() healthMap["version"] = c.GetAppVersion() diff --git a/pkg/gofr/container/health_test.go b/pkg/gofr/container/health_test.go index 4efdfa26a..1c331353b 100644 --- a/pkg/gofr/container/health_test.go +++ b/pkg/gofr/container/health_test.go @@ -2,6 +2,7 @@ package container import ( "context" + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -34,32 +35,41 @@ func TestContainer_Health(t *testing.T) { for i, tc := range tests { expected := map[string]interface{}{ "redis": datasource.Health{ - Status: tc.datasourceHealth, - Details: map[string]interface{}{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ "host": "localhost:6379", "error": "redis not connected", }, }, + "mongo": datasource.Health{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "mongo not connected", + }, + }, + "clickHouse": datasource.Health{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "clickhouse not connected", + }, + }, + "cassandra": datasource.Health{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "cassandra not connected", + }, + }, + "sql": &datasource.Health{ - Status: tc.datasourceHealth, - Details: map[string]interface{}{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ "host": "localhost:3306/test", "stats": sql.DBStats{ - MaxOpenConnections: 0, - OpenConnections: 1, - InUse: 0, - Idle: 1, - WaitCount: 0, - WaitDuration: 0, - MaxIdleClosed: 0, - MaxIdleTimeClosed: 0, - MaxLifetimeClosed: 0, + MaxOpenConnections: 0, OpenConnections: 1, InUse: 0, Idle: 1, WaitCount: 0, + WaitDuration: 0, MaxIdleClosed: 0, MaxIdleTimeClosed: 0, MaxLifetimeClosed: 0, }, }, }, "test-service": &service.Health{ - Status: "UP", - Details: map[string]interface{}{ + Status: "UP", Details: map[string]interface{}{ "host": strings.TrimPrefix(srv.URL, "http://"), }, }, @@ -68,42 +78,67 @@ func TestContainer_Health(t *testing.T) { "version": "test", } + expectedJSONdata, _ := json.Marshal(expected) + c, mocks := NewMockContainer(t) + registerMocks(mocks, tc.datasourceHealth) + c.appName = "test-app" c.appVersion = "test" c.Services = make(map[string]service.HTTP) c.Services["test-service"] = service.NewHTTPService(srv.URL, logger, nil) - mocks.SQL.EXPECT().HealthCheck().Return(&datasource.Health{ - Status: tc.datasourceHealth, - Details: map[string]interface{}{ - "host": "localhost:3306/test", - "stats": sql.DBStats{ - MaxOpenConnections: 0, - OpenConnections: 1, - InUse: 0, - Idle: 1, - WaitCount: 0, - WaitDuration: 0, - MaxIdleClosed: 0, - MaxIdleTimeClosed: 0, - MaxLifetimeClosed: 0, - }, - }, - }) + healthData := c.Health(context.Background()) - mocks.Redis.EXPECT().HealthCheck().Return(datasource.Health{ - Status: tc.datasourceHealth, - Details: map[string]interface{}{ - "host": "localhost:6379", - "error": "redis not connected", + jsonData, _ := json.Marshal(healthData) + + assert.Equal(t, string(expectedJSONdata), string(jsonData), "TEST[%d], Failed.\n%s", i, tc.desc) + } +} + +func registerMocks(mocks Mocks, health string) { + mocks.SQL.EXPECT().HealthCheck().Return(&datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:3306/test", + "stats": sql.DBStats{ + MaxOpenConnections: 0, OpenConnections: 1, InUse: 0, Idle: 1, WaitCount: 0, + WaitDuration: 0, MaxIdleClosed: 0, MaxIdleTimeClosed: 0, MaxLifetimeClosed: 0, }, - }) + }, + }) - healthData := c.Health(context.Background()) + mocks.Redis.EXPECT().HealthCheck().Return(datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "redis not connected", + }, + }) - assert.Equal(t, expected, healthData, "TEST[%d], Failed.\n%s", i, tc.desc) - } + mocks.Mongo.EXPECT().HealthCheck(context.Background()).Return(datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "mongo not connected", + }, + }, nil) + + mocks.Cassandra.EXPECT().HealthCheck(context.Background()).Return(datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "cassandra not connected", + }, + }, nil) + + mocks.Clickhouse.EXPECT().HealthCheck(context.Background()).Return(datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:6379", + "error": "clickhouse not connected", + }, + }, nil) } diff --git a/pkg/gofr/container/mock_container.go b/pkg/gofr/container/mock_container.go index 5a6ada69a..afa868224 100644 --- a/pkg/gofr/container/mock_container.go +++ b/pkg/gofr/container/mock_container.go @@ -22,6 +22,8 @@ type Mocks struct { } func NewMockContainer(t *testing.T) (*Container, Mocks) { + t.Helper() + container := &Container{} container.Logger = logging.NewLogger(logging.DEBUG) diff --git a/pkg/gofr/container/mock_datasources.go b/pkg/gofr/container/mock_datasources.go index bcdd509e2..fe01a93b2 100644 --- a/pkg/gofr/container/mock_datasources.go +++ b/pkg/gofr/container/mock_datasources.go @@ -17,6 +17,7 @@ import ( redis "github.com/redis/go-redis/v9" gomock "go.uber.org/mock/gomock" + datasource "gofr.dev/pkg/gofr/datasource" sql0 "gofr.dev/pkg/gofr/datasource/sql" ) @@ -7327,6 +7328,21 @@ func (mr *MockCassandraMockRecorder) ExecCAS(dest, stmt any, values ...any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandra)(nil).ExecCAS), varargs...) } +// HealthCheck mocks base method. +func (m *MockCassandra) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockCassandraMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandra)(nil).HealthCheck), arg0) +} + // Query mocks base method. func (m *MockCassandra) Query(dest any, stmt string, values ...any) error { m.ctrl.T.Helper() @@ -7346,6 +7362,138 @@ func (mr *MockCassandraMockRecorder) Query(dest, stmt any, values ...any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandra)(nil).Query), varargs...) } +// MockCassandraProvider is a mock of CassandraProvider interface. +type MockCassandraProvider struct { + ctrl *gomock.Controller + recorder *MockCassandraProviderMockRecorder +} + +// MockCassandraProviderMockRecorder is the mock recorder for MockCassandraProvider. +type MockCassandraProviderMockRecorder struct { + mock *MockCassandraProvider +} + +// NewMockCassandraProvider creates a new mock instance. +func NewMockCassandraProvider(ctrl *gomock.Controller) *MockCassandraProvider { + mock := &MockCassandraProvider{ctrl: ctrl} + mock.recorder = &MockCassandraProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCassandraProvider) EXPECT() *MockCassandraProviderMockRecorder { + return m.recorder +} + +// Connect mocks base method. +func (m *MockCassandraProvider) Connect() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Connect") +} + +// Connect indicates an expected call of Connect. +func (mr *MockCassandraProviderMockRecorder) Connect() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockCassandraProvider)(nil).Connect)) +} + +// Exec mocks base method. +func (m *MockCassandraProvider) Exec(stmt string, values ...any) error { + m.ctrl.T.Helper() + varargs := []any{stmt} + for _, a := range values { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Exec", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Exec indicates an expected call of Exec. +func (mr *MockCassandraProviderMockRecorder) Exec(stmt any, values ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{stmt}, values...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandraProvider)(nil).Exec), varargs...) +} + +// ExecCAS mocks base method. +func (m *MockCassandraProvider) ExecCAS(dest any, stmt string, values ...any) (bool, error) { + m.ctrl.T.Helper() + varargs := []any{dest, stmt} + for _, a := range values { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExecCAS", varargs...) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecCAS indicates an expected call of ExecCAS. +func (mr *MockCassandraProviderMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{dest, stmt}, values...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandraProvider)(nil).ExecCAS), varargs...) +} + +// HealthCheck mocks base method. +func (m *MockCassandraProvider) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockCassandraProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandraProvider)(nil).HealthCheck), arg0) +} + +// Query mocks base method. +func (m *MockCassandraProvider) Query(dest any, stmt string, values ...any) error { + m.ctrl.T.Helper() + varargs := []any{dest, stmt} + for _, a := range values { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Query", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Query indicates an expected call of Query. +func (mr *MockCassandraProviderMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{dest, stmt}, values...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandraProvider)(nil).Query), varargs...) +} + +// UseLogger mocks base method. +func (m *MockCassandraProvider) UseLogger(logger any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseLogger", logger) +} + +// UseLogger indicates an expected call of UseLogger. +func (mr *MockCassandraProviderMockRecorder) UseLogger(logger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockCassandraProvider)(nil).UseLogger), logger) +} + +// UseMetrics mocks base method. +func (m *MockCassandraProvider) UseMetrics(metrics any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseMetrics", metrics) +} + +// UseMetrics indicates an expected call of UseMetrics. +func (mr *MockCassandraProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockCassandraProvider)(nil).UseMetrics), metrics) +} + // MockClickhouse is a mock of Clickhouse interface. type MockClickhouse struct { ctrl *gomock.Controller @@ -7407,6 +7555,21 @@ func (mr *MockClickhouseMockRecorder) Exec(ctx, query any, args ...any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouse)(nil).Exec), varargs...) } +// HealthCheck mocks base method. +func (m *MockClickhouse) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockClickhouseMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouse)(nil).HealthCheck), arg0) +} + // Select mocks base method. func (m *MockClickhouse) Select(ctx context.Context, dest any, query string, args ...any) error { m.ctrl.T.Helper() @@ -7499,6 +7662,21 @@ func (mr *MockClickhouseProviderMockRecorder) Exec(ctx, query any, args ...any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouseProvider)(nil).Exec), varargs...) } +// HealthCheck mocks base method. +func (m *MockClickhouseProvider) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockClickhouseProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouseProvider)(nil).HealthCheck), arg0) +} + // Select mocks base method. func (m *MockClickhouseProvider) Select(ctx context.Context, dest any, query string, args ...any) error { m.ctrl.T.Helper() @@ -7542,123 +7720,6 @@ func (mr *MockClickhouseProviderMockRecorder) UseMetrics(metrics any) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockClickhouseProvider)(nil).UseMetrics), metrics) } -// MockCassandraProvider is a mock of CassandraProvider interface. -type MockCassandraProvider struct { - ctrl *gomock.Controller - recorder *MockCassandraProviderMockRecorder -} - -// MockCassandraProviderMockRecorder is the mock recorder for MockCassandraProvider. -type MockCassandraProviderMockRecorder struct { - mock *MockCassandraProvider -} - -// NewMockCassandraProvider creates a new mock instance. -func NewMockCassandraProvider(ctrl *gomock.Controller) *MockCassandraProvider { - mock := &MockCassandraProvider{ctrl: ctrl} - mock.recorder = &MockCassandraProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCassandraProvider) EXPECT() *MockCassandraProviderMockRecorder { - return m.recorder -} - -// Connect mocks base method. -func (m *MockCassandraProvider) Connect() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Connect") -} - -// Connect indicates an expected call of Connect. -func (mr *MockCassandraProviderMockRecorder) Connect() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockCassandraProvider)(nil).Connect)) -} - -// Exec mocks base method. -func (m *MockCassandraProvider) Exec(stmt string, values ...any) error { - m.ctrl.T.Helper() - varargs := []any{stmt} - for _, a := range values { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Exec", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// Exec indicates an expected call of Exec. -func (mr *MockCassandraProviderMockRecorder) Exec(stmt any, values ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{stmt}, values...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandraProvider)(nil).Exec), varargs...) -} - -// ExecCAS mocks base method. -func (m *MockCassandraProvider) ExecCAS(dest any, stmt string, values ...any) (bool, error) { - m.ctrl.T.Helper() - varargs := []any{dest, stmt} - for _, a := range values { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ExecCAS", varargs...) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ExecCAS indicates an expected call of ExecCAS. -func (mr *MockCassandraProviderMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{dest, stmt}, values...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandraProvider)(nil).ExecCAS), varargs...) -} - -// Query mocks base method. -func (m *MockCassandraProvider) Query(dest any, stmt string, values ...any) error { - m.ctrl.T.Helper() - varargs := []any{dest, stmt} - for _, a := range values { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Query", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// Query indicates an expected call of Query. -func (mr *MockCassandraProviderMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{dest, stmt}, values...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandraProvider)(nil).Query), varargs...) -} - -// UseLogger mocks base method. -func (m *MockCassandraProvider) UseLogger(logger any) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "UseLogger", logger) -} - -// UseLogger indicates an expected call of UseLogger. -func (mr *MockCassandraProviderMockRecorder) UseLogger(logger any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockCassandraProvider)(nil).UseLogger), logger) -} - -// UseMetrics mocks base method. -func (m *MockCassandraProvider) UseMetrics(metrics any) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "UseMetrics", metrics) -} - -// UseMetrics indicates an expected call of UseMetrics. -func (mr *MockCassandraProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockCassandraProvider)(nil).UseMetrics), metrics) -} - // MockMongo is a mock of Mongo interface. type MockMongo struct { ctrl *gomock.Controller @@ -7697,6 +7758,20 @@ func (mr *MockMongoMockRecorder) CountDocuments(ctx, collection, filter any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountDocuments", reflect.TypeOf((*MockMongo)(nil).CountDocuments), ctx, collection, filter) } +// CreateCollection mocks base method. +func (m *MockMongo) CreateCollection(ctx context.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCollection", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateCollection indicates an expected call of CreateCollection. +func (mr *MockMongoMockRecorder) CreateCollection(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockMongo)(nil).CreateCollection), ctx, name) +} + // DeleteMany mocks base method. func (m *MockMongo) DeleteMany(ctx context.Context, collection string, filter any) (int64, error) { m.ctrl.T.Helper() @@ -7769,6 +7844,21 @@ func (mr *MockMongoMockRecorder) FindOne(ctx, collection, filter, result any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockMongo)(nil).FindOne), ctx, collection, filter, result) } +// HealthCheck mocks base method. +func (m *MockMongo) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockMongoMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockMongo)(nil).HealthCheck), arg0) +} + // InsertMany mocks base method. func (m *MockMongo) InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) { m.ctrl.T.Helper() @@ -7799,6 +7889,21 @@ func (mr *MockMongoMockRecorder) InsertOne(ctx, collection, document any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOne", reflect.TypeOf((*MockMongo)(nil).InsertOne), ctx, collection, document) } +// StartSession mocks base method. +func (m *MockMongo) StartSession() (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartSession") + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartSession indicates an expected call of StartSession. +func (mr *MockMongoMockRecorder) StartSession() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartSession", reflect.TypeOf((*MockMongo)(nil).StartSession)) +} + // UpdateByID mocks base method. func (m *MockMongo) UpdateByID(ctx context.Context, collection string, id, update any) (int64, error) { m.ctrl.T.Helper() @@ -7843,6 +7948,83 @@ func (mr *MockMongoMockRecorder) UpdateOne(ctx, collection, filter, update any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOne", reflect.TypeOf((*MockMongo)(nil).UpdateOne), ctx, collection, filter, update) } +// MockTransaction is a mock of Transaction interface. +type MockTransaction struct { + ctrl *gomock.Controller + recorder *MockTransactionMockRecorder +} + +// MockTransactionMockRecorder is the mock recorder for MockTransaction. +type MockTransactionMockRecorder struct { + mock *MockTransaction +} + +// NewMockTransaction creates a new mock instance. +func NewMockTransaction(ctrl *gomock.Controller) *MockTransaction { + mock := &MockTransaction{ctrl: ctrl} + mock.recorder = &MockTransactionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTransaction) EXPECT() *MockTransactionMockRecorder { + return m.recorder +} + +// AbortTransaction mocks base method. +func (m *MockTransaction) AbortTransaction(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AbortTransaction", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AbortTransaction indicates an expected call of AbortTransaction. +func (mr *MockTransactionMockRecorder) AbortTransaction(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortTransaction", reflect.TypeOf((*MockTransaction)(nil).AbortTransaction), arg0) +} + +// CommitTransaction mocks base method. +func (m *MockTransaction) CommitTransaction(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommitTransaction", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CommitTransaction indicates an expected call of CommitTransaction. +func (mr *MockTransactionMockRecorder) CommitTransaction(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitTransaction", reflect.TypeOf((*MockTransaction)(nil).CommitTransaction), arg0) +} + +// EndSession mocks base method. +func (m *MockTransaction) EndSession(arg0 context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EndSession", arg0) +} + +// EndSession indicates an expected call of EndSession. +func (mr *MockTransactionMockRecorder) EndSession(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndSession", reflect.TypeOf((*MockTransaction)(nil).EndSession), arg0) +} + +// StartTransaction mocks base method. +func (m *MockTransaction) StartTransaction() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartTransaction") + ret0, _ := ret[0].(error) + return ret0 +} + +// StartTransaction indicates an expected call of StartTransaction. +func (mr *MockTransactionMockRecorder) StartTransaction() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTransaction", reflect.TypeOf((*MockTransaction)(nil).StartTransaction)) +} + // MockMongoProvider is a mock of MongoProvider interface. type MockMongoProvider struct { ctrl *gomock.Controller @@ -7893,6 +8075,20 @@ func (mr *MockMongoProviderMockRecorder) CountDocuments(ctx, collection, filter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountDocuments", reflect.TypeOf((*MockMongoProvider)(nil).CountDocuments), ctx, collection, filter) } +// CreateCollection mocks base method. +func (m *MockMongoProvider) CreateCollection(ctx context.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCollection", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateCollection indicates an expected call of CreateCollection. +func (mr *MockMongoProviderMockRecorder) CreateCollection(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockMongoProvider)(nil).CreateCollection), ctx, name) +} + // DeleteMany mocks base method. func (m *MockMongoProvider) DeleteMany(ctx context.Context, collection string, filter any) (int64, error) { m.ctrl.T.Helper() @@ -7965,6 +8161,21 @@ func (mr *MockMongoProviderMockRecorder) FindOne(ctx, collection, filter, result return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockMongoProvider)(nil).FindOne), ctx, collection, filter, result) } +// HealthCheck mocks base method. +func (m *MockMongoProvider) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockMongoProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockMongoProvider)(nil).HealthCheck), arg0) +} + // InsertMany mocks base method. func (m *MockMongoProvider) InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) { m.ctrl.T.Helper() @@ -7995,6 +8206,21 @@ func (mr *MockMongoProviderMockRecorder) InsertOne(ctx, collection, document any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOne", reflect.TypeOf((*MockMongoProvider)(nil).InsertOne), ctx, collection, document) } +// StartSession mocks base method. +func (m *MockMongoProvider) StartSession() (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartSession") + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartSession indicates an expected call of StartSession. +func (mr *MockMongoProviderMockRecorder) StartSession() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartSession", reflect.TypeOf((*MockMongoProvider)(nil).StartSession)) +} + // UpdateByID mocks base method. func (m *MockMongoProvider) UpdateByID(ctx context.Context, collection string, id, update any) (int64, error) { m.ctrl.T.Helper() @@ -8121,3 +8347,41 @@ func (mr *MockproviderMockRecorder) UseMetrics(metrics any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*Mockprovider)(nil).UseMetrics), metrics) } + +// MockHealthChecker is a mock of HealthChecker interface. +type MockHealthChecker struct { + ctrl *gomock.Controller + recorder *MockHealthCheckerMockRecorder +} + +// MockHealthCheckerMockRecorder is the mock recorder for MockHealthChecker. +type MockHealthCheckerMockRecorder struct { + mock *MockHealthChecker +} + +// NewMockHealthChecker creates a new mock instance. +func NewMockHealthChecker(ctrl *gomock.Controller) *MockHealthChecker { + mock := &MockHealthChecker{ctrl: ctrl} + mock.recorder = &MockHealthCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHealthChecker) EXPECT() *MockHealthCheckerMockRecorder { + return m.recorder +} + +// HealthCheck mocks base method. +func (m *MockHealthChecker) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockHealthCheckerMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockHealthChecker)(nil).HealthCheck), arg0) +} diff --git a/pkg/gofr/context_test.go b/pkg/gofr/context_test.go index 46ab401fb..bb6376903 100644 --- a/pkg/gofr/context_test.go +++ b/pkg/gofr/context_test.go @@ -16,7 +16,7 @@ import ( func Test_newContextSuccess(t *testing.T) { httpRequest, err := http.NewRequestWithContext(context.Background(), - http.MethodPost, "/test", bytes.NewBuffer([]byte(`{"key":"value"}`))) + http.MethodPost, "/test", bytes.NewBufferString(`{"key":"value"}`)) httpRequest.Header.Set("content-type", "application/json") if err != nil { diff --git a/pkg/gofr/cron.go b/pkg/gofr/cron.go index 3e169471b..0b9745f6d 100644 --- a/pkg/gofr/cron.go +++ b/pkg/gofr/cron.go @@ -136,36 +136,37 @@ func mergeDays(j *job) { } // parsePart parse individual schedule part from schedule string. -func parsePart(s string, min, max int) (map[int]struct{}, error) { +func parsePart(s string, minValue, maxValue int) (map[int]struct{}, error) { // wildcard pattern if s == "*" { - return getDefaultJobField(min, max, 1), nil + return getDefaultJobField(minValue, maxValue, 1), nil } // */2 1-59/5 pattern if matches := matchN.FindStringSubmatch(s); matches != nil { - return parseSteps(s, matches[1], matches[2], min, max) + return parseSteps(s, matches[1], matches[2], minValue, maxValue) } // 1,2,4 or 1,2,10-15,20,30-45 pattern - return parseRange(s, min, max) + return parseRange(s, minValue, maxValue) } -func parseSteps(s, match1, match2 string, min, max int) (map[int]struct{}, error) { - localMin := min - localMax := max +func parseSteps(s, match1, match2 string, minValue, maxValue int) (map[int]struct{}, error) { + localMin := minValue + localMax := maxValue if match1 != "" && match1 != "*" { - if rng := matchRange.FindStringSubmatch(match1); rng != nil { - localMin, _ = strconv.Atoi(rng[1]) - localMax, _ = strconv.Atoi(rng[2]) - - if localMin < min || localMax > max { - return nil, errOutOfRange{rng[1], s, min, max} - } - } else { + rng := matchRange.FindStringSubmatch(match1) + if rng == nil { return nil, errParsing{match1, s} } + + localMin, _ = strconv.Atoi(rng[1]) + localMax, _ = strconv.Atoi(rng[2]) + + if localMin < minValue || localMax > maxValue { + return nil, errOutOfRange{rng[1], s, minValue, maxValue} + } } n, _ := strconv.Atoi(match2) @@ -173,29 +174,36 @@ func parseSteps(s, match1, match2 string, min, max int) (map[int]struct{}, error return getDefaultJobField(localMin, localMax, n), nil } -func parseRange(s string, min, max int) (map[int]struct{}, error) { +func parseRange(s string, minValue, maxValue int) (map[int]struct{}, error) { r := make(map[int]struct{}) parts := strings.Split(s, ",") for _, x := range parts { - if rng := matchRange.FindStringSubmatch(x); rng != nil { - localMin, _ := strconv.Atoi(rng[1]) - localMax, _ := strconv.Atoi(rng[2]) + rng := matchRange.FindStringSubmatch(x) - if localMin < min || localMax > max { - return nil, errOutOfRange{x, s, min, max} + if rng == nil { + i, err := strconv.Atoi(x) + if err != nil { + return nil, errParsing{x, s} } - r = getDefaultJobField(localMin, localMax, 1) - } else if i, err := strconv.Atoi(x); err == nil { - if i < min || i > max { - return nil, errOutOfRange{i, s, min, max} + if i < minValue || i > maxValue { + return nil, errOutOfRange{i, s, minValue, maxValue} } r[i] = struct{}{} - } else { - return nil, errParsing{x, s} + + continue } + + localMin, _ := strconv.Atoi(rng[1]) + localMax, _ := strconv.Atoi(rng[2]) + + if localMin < minValue || localMax > maxValue { + return nil, errOutOfRange{x, s, minValue, maxValue} + } + + r = getDefaultJobField(localMin, localMax, 1) } if len(r) == 0 { @@ -205,10 +213,10 @@ func parseRange(s string, min, max int) (map[int]struct{}, error) { return r, nil } -func getDefaultJobField(min, max, incr int) map[int]struct{} { +func getDefaultJobField(minValue, maxValue, incr int) map[int]struct{} { r := make(map[int]struct{}) - for i := min; i <= max; i += incr { + for i := minValue; i <= maxValue; i += incr { r[i] = struct{}{} } diff --git a/pkg/gofr/datasource/cassandra/cassandra.go b/pkg/gofr/datasource/cassandra/cassandra.go index 287cfd631..5d01608f5 100644 --- a/pkg/gofr/datasource/cassandra/cassandra.go +++ b/pkg/gofr/datasource/cassandra/cassandra.go @@ -2,6 +2,7 @@ package cassandra import ( "context" + "errors" "reflect" "regexp" "strings" @@ -33,6 +34,8 @@ type Client struct { metrics Metrics } +var errStatusDown = errors.New("status down") + // New initializes Cassandra driver with the provided configuration. // The Connect method must be called to establish a connection to Cassandra. // Usage: @@ -297,7 +300,7 @@ type Health struct { } // HealthCheck checks the health of the Cassandra. -func (c *Client) HealthCheck() interface{} { +func (c *Client) HealthCheck(context.Context) (any, error) { const ( statusDown = "DOWN" statusUp = "UP" @@ -314,7 +317,7 @@ func (c *Client) HealthCheck() interface{} { h.Status = statusDown h.Details["message"] = "cassandra not connected" - return &h + return &h, errStatusDown } err := c.cassandra.session.query("SELECT now() FROM system.local").exec() @@ -322,12 +325,12 @@ func (c *Client) HealthCheck() interface{} { h.Status = statusDown h.Details["message"] = err.Error() - return &h + return &h, errStatusDown } h.Status = statusUp - return &h + return &h, nil } // cassandraIterator implements iterator interface. diff --git a/pkg/gofr/datasource/cassandra/cassandra_test.go b/pkg/gofr/datasource/cassandra/cassandra_test.go index 943538843..7806f511b 100644 --- a/pkg/gofr/datasource/cassandra/cassandra_test.go +++ b/pkg/gofr/datasource/cassandra/cassandra_test.go @@ -246,6 +246,7 @@ func Test_HealthCheck(t *testing.T) { desc string mockCall func() expHealth *Health + err error }{ {"success case", func() { mockSession.EXPECT().query(query).Return(mockQuery).Times(1) @@ -253,7 +254,7 @@ func Test_HealthCheck(t *testing.T) { }, &Health{ Status: "UP", Details: map[string]interface{}{"host": client.config.Hosts, "keyspace": client.config.Keyspace}, - }}, + }, nil}, {"failure case: exec error", func() { mockSession.EXPECT().query(query).Return(mockQuery).Times(1) mockQuery.EXPECT().exec().Return(errMock).Times(1) @@ -261,7 +262,7 @@ func Test_HealthCheck(t *testing.T) { Status: "DOWN", Details: map[string]interface{}{"host": client.config.Hosts, "keyspace": client.config.Keyspace, "message": errMock.Error()}, - }}, + }, errStatusDown}, {"failure case: cassandra not initializes", func() { client.cassandra.session = nil @@ -271,14 +272,15 @@ func Test_HealthCheck(t *testing.T) { Status: "DOWN", Details: map[string]interface{}{"host": client.config.Hosts, "keyspace": client.config.Keyspace, "message": "cassandra not connected"}, - }}, + }, errStatusDown}, } for i, tc := range testCases { tc.mockCall() - health := client.HealthCheck() + health, err := client.HealthCheck(context.Background()) + assert.Equal(t, tc.err, err) assert.Equalf(t, tc.expHealth, health, "TEST[%d], Failed.\n%s", i, tc.desc) } } diff --git a/pkg/gofr/datasource/clickhouse/clickhouse.go b/pkg/gofr/datasource/clickhouse/clickhouse.go index f431ff925..ae975f322 100644 --- a/pkg/gofr/datasource/clickhouse/clickhouse.go +++ b/pkg/gofr/datasource/clickhouse/clickhouse.go @@ -2,6 +2,7 @@ package clickhouse import ( "context" + "errors" "strings" "time" @@ -22,6 +23,8 @@ type client struct { metrics Metrics } +var errStatusDown = errors.New("status down") + // New initializes Clickhouse client with the provided configuration. // Metrics, Logger has to be initialized before calling the Connect method. // Usage: @@ -165,12 +168,8 @@ type Health struct { Details map[string]interface{} `json:"details,omitempty"` } -func (h *Health) GetStatus() string { - return h.Status -} - // HealthCheck checks the health of the MongoDB client by pinging the database. -func (c *client) HealthCheck() interface{} { +func (c *client) HealthCheck(ctx context.Context) (any, error) { h := Health{ Details: make(map[string]interface{}), } @@ -178,17 +177,14 @@ func (c *client) HealthCheck() interface{} { h.Details["host"] = c.config.Hosts h.Details["database"] = c.config.Database - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - err := c.conn.Ping(ctx) if err != nil { h.Status = "DOWN" - return &h + return &h, errStatusDown } h.Status = "UP" - return &h + return &h, nil } diff --git a/pkg/gofr/datasource/clickhouse/clickhouse_test.go b/pkg/gofr/datasource/clickhouse/clickhouse_test.go index 48304ee7a..d1f8b1f1a 100644 --- a/pkg/gofr/datasource/clickhouse/clickhouse_test.go +++ b/pkg/gofr/datasource/clickhouse/clickhouse_test.go @@ -9,9 +9,8 @@ import ( "testing" "time" - "go.uber.org/mock/gomock" - "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" ) func getClickHouseTestConnection(t *testing.T) (*MockConn, *MockMetrics, client) { @@ -81,7 +80,7 @@ func Test_ClickHouse_HealthUP(t *testing.T) { mockConn.EXPECT().Ping(gomock.Any()).Return(nil) - resp := c.HealthCheck() + resp, _ := c.HealthCheck(context.Background()) assert.Contains(t, fmt.Sprint(resp), "UP") } @@ -91,7 +90,9 @@ func Test_ClickHouse_HealthDOWN(t *testing.T) { mockConn.EXPECT().Ping(gomock.Any()).Return(sql.ErrConnDone) - resp := c.HealthCheck() + resp, err := c.HealthCheck(context.Background()) + + assert.ErrorIs(t, err, errStatusDown) assert.Contains(t, fmt.Sprint(resp), "DOWN") } diff --git a/pkg/gofr/datasource/mongo/mongo.go b/pkg/gofr/datasource/mongo/mongo.go index cda868be6..9f3bf5557 100644 --- a/pkg/gofr/datasource/mongo/mongo.go +++ b/pkg/gofr/datasource/mongo/mongo.go @@ -2,6 +2,8 @@ package mongo import ( "context" + "errors" + "fmt" "time" "go.mongodb.org/mongo-driver/bson" @@ -21,10 +23,17 @@ type Client struct { } type Config struct { - URI string + Host string + User string + Password string + Port int Database string + // Deprecated Provide Host User Password Port Instead and driver will generate the URI + URI string } +var errStatusDown = errors.New("status down") + /* Developer Note: We could have accepted logger and metrics as part of the factory function `New`, but when mongo driver is initialised in GoFr, We want to ensure that the user need not to provides logger and metrics and then connect to the database, @@ -60,7 +69,14 @@ func (c *Client) UseMetrics(metrics interface{}) { func (c *Client) Connect() { c.logger.Logf("connecting to mongoDB at %v to database %v", c.config.URI, c.config.Database) - m, err := mongo.Connect(context.Background(), options.Client().ApplyURI(c.config.URI)) + uri := c.config.URI + + if uri == "" { + uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=admin", + c.config.User, c.config.Password, c.config.Host, c.config.Port, c.config.Database) + } + + m, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri)) if err != nil { c.logger.Errorf("error connecting to mongoDB, err:%v", err) @@ -187,6 +203,13 @@ func (c *Client) Drop(ctx context.Context, collection string) error { return c.Database.Collection(collection).Drop(ctx) } +// CreateCollection creates the specified collection in the database. +func (c *Client) CreateCollection(ctx context.Context, name string) error { + defer c.postProcess(&QueryLog{Query: "createCollection", Collection: name}, time.Now()) + + return c.Database.CreateCollection(ctx, name) +} + func (c *Client) postProcess(ql *QueryLog, startTime time.Time) { duration := time.Since(startTime).Milliseconds() @@ -204,7 +227,7 @@ type Health struct { } // HealthCheck checks the health of the MongoDB client by pinging the database. -func (c *Client) HealthCheck() interface{} { +func (c *Client) HealthCheck(ctx context.Context) (any, error) { h := Health{ Details: make(map[string]interface{}), } @@ -212,17 +235,38 @@ func (c *Client) HealthCheck() interface{} { h.Details["host"] = c.uri h.Details["database"] = c.database - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - err := c.Database.Client().Ping(ctx, readpref.Primary()) if err != nil { h.Status = "DOWN" - return &h + return &h, errStatusDown } h.Status = "UP" - return &h + return &h, nil +} + +func (c *Client) StartSession() (interface{}, error) { + defer c.postProcess(&QueryLog{Query: "startSession"}, time.Now()) + + s, err := c.Client().StartSession() + ses := &session{s} + + return ses, err +} + +type session struct { + mongo.Session +} + +func (s *session) StartTransaction() error { + return s.Session.StartTransaction() +} + +type Transaction interface { + StartTransaction() error + AbortTransaction(context.Context) error + CommitTransaction(context.Context) error + EndSession(context.Context) } diff --git a/pkg/gofr/datasource/mongo/mongo_test.go b/pkg/gofr/datasource/mongo/mongo_test.go index a19311cd4..b04b70178 100644 --- a/pkg/gofr/datasource/mongo/mongo_test.go +++ b/pkg/gofr/datasource/mongo/mongo_test.go @@ -3,7 +3,6 @@ package mongo import ( "context" "fmt" - "testing" "github.com/stretchr/testify/assert" @@ -20,7 +19,7 @@ func Test_NewMongoClient(t *testing.T) { metrics.EXPECT().NewHistogram("app_mongo_stats", "Response time of MONGO queries in milliseconds.", gomock.Any()) - client := New(Config{URI: "mongodb://localhost:27017", Database: "test"}) + client := New(Config{Database: "test", Host: "localhost", Port: 27017, User: "admin"}) client.UseLogger(NewMockLogger(DEBUG)) client.UseMetrics(metrics) client.Connect() @@ -109,6 +108,29 @@ func Test_InsertCommands(t *testing.T) { }) } +func Test_CreateCollection(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(1) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("createCollection", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + err := cl.CreateCollection(context.Background(), mt.Coll.Name()) + + assert.Nil(t, err) + }) +} + func Test_FindMultipleCommands(t *testing.T) { // Create a connected client using the mock database mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -402,6 +424,56 @@ func Test_Drop(t *testing.T) { }) } +func TestClient_StartSession(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + // Set up the mock expectation for the metrics recording + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).AnyTimes() + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("StartSessionCommitTransactionSuccess", func(mt *mtest.T) { + cl.Database = mt.DB + + // Add mock responses if necessary + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + // Call the StartSession method + sess, err := cl.StartSession() + ses, ok := sess.(Transaction) + if ok { + err = ses.StartTransaction() + } + + assert.Nil(t, err) + + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + doc := map[string]interface{}{"name": "Aryan"} + + resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc) + + assert.NotNil(t, resp) + assert.Nil(t, err) + + err = ses.CommitTransaction(context.Background()) + + assert.Nil(t, err) + + ses.EndSession(context.Background()) + + // Assert that there was no error + assert.Nil(t, err) + }) +} + func Test_HealthCheck(t *testing.T) { // Create a connected client using the mock database mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -416,8 +488,9 @@ func Test_HealthCheck(t *testing.T) { cl.Database = mt.DB mt.AddMockResponses(mtest.CreateSuccessResponse()) - resp := cl.HealthCheck() + resp, err := cl.HealthCheck(context.Background()) + assert.NoError(t, err) assert.Contains(t, fmt.Sprint(resp), "UP") }) @@ -429,7 +502,9 @@ func Test_HealthCheck(t *testing.T) { Message: "duplicate key error", })) - resp := cl.HealthCheck() + resp, err := cl.HealthCheck(context.Background()) + + assert.ErrorIs(t, err, errStatusDown) assert.Contains(t, fmt.Sprint(resp), "DOWN") }) diff --git a/pkg/gofr/datasource/pubsub/google/google_test.go b/pkg/gofr/datasource/pubsub/google/google_test.go index 5b8a1f09d..27f94f2b4 100644 --- a/pkg/gofr/datasource/pubsub/google/google_test.go +++ b/pkg/gofr/datasource/pubsub/google/google_test.go @@ -17,6 +17,8 @@ import ( ) func getGoogleClient(t *testing.T) *gcPubSub.Client { + t.Helper() + srv := pstest.NewServer() conn, err := grpc.NewClient(srv.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/pkg/gofr/datasource/pubsub/google/health.go b/pkg/gofr/datasource/pubsub/google/health.go index 2f4c59630..a2306da2f 100644 --- a/pkg/gofr/datasource/pubsub/google/health.go +++ b/pkg/gofr/datasource/pubsub/google/health.go @@ -5,8 +5,9 @@ import ( "errors" "time" - "gofr.dev/pkg/gofr/datasource" "google.golang.org/api/iterator" + + "gofr.dev/pkg/gofr/datasource" ) func (g *googleClient) Health() (health datasource.Health) { diff --git a/pkg/gofr/datasource/pubsub/kafka/health.go b/pkg/gofr/datasource/pubsub/kafka/health.go index a065d570e..ab438eb15 100644 --- a/pkg/gofr/datasource/pubsub/kafka/health.go +++ b/pkg/gofr/datasource/pubsub/kafka/health.go @@ -21,7 +21,7 @@ func (k *kafkaClient) Health() (health datasource.Health) { health.Details["writers"] = k.getWriterStatsAsMap() health.Details["readers"] = k.getReaderStatsAsMap() - return + return health } func (k *kafkaClient) getReaderStatsAsMap() []interface{} { diff --git a/pkg/gofr/datasource/pubsub/mqtt/interface.go b/pkg/gofr/datasource/pubsub/mqtt/interface.go index 209ef3816..f6f1866b0 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/interface.go +++ b/pkg/gofr/datasource/pubsub/mqtt/interface.go @@ -8,6 +8,10 @@ import ( "gofr.dev/pkg/gofr/datasource" ) +//go:generate go run go.uber.org/mock/mockgen -destination=mock_client.go -package=mqtt github.com/eclipse/paho.mqtt.golang Client +//go:generate go run go.uber.org/mock/mockgen -destination=mock_token.go -package=mqtt github.com/eclipse/paho.mqtt.golang Token +//go:generate go run go.uber.org/mock/mockgen -source=interface.go -destination=mock_interfaces.go -package=mqtt + type Logger interface { Infof(format string, args ...interface{}) Debug(args ...interface{}) diff --git a/pkg/gofr/datasource/pubsub/mqtt/message_test.go b/pkg/gofr/datasource/pubsub/mqtt/message_test.go index 3d1d88923..6f7dcef86 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/message_test.go +++ b/pkg/gofr/datasource/pubsub/mqtt/message_test.go @@ -5,36 +5,42 @@ import ( ) func TestMessage(_ *testing.T) { - msg := message{msg: mockMessage{}} + m := message{msg: mockMessage{}} - msg.Commit() + m.Commit() } type mockMessage struct { + duplicate bool + qos int + retained bool + topic string + messageID int + pyload string } func (m mockMessage) Duplicate() bool { - return false + return m.duplicate } func (m mockMessage) Qos() byte { - return 0 + return byte(m.qos) } func (m mockMessage) Retained() bool { - return false + return m.retained } func (m mockMessage) Topic() string { - return "" + return m.topic } func (m mockMessage) MessageID() uint16 { - return 1 + return uint16(m.messageID) } func (m mockMessage) Payload() []byte { - return nil + return []byte(m.pyload) } func (m mockMessage) Ack() { diff --git a/pkg/gofr/datasource/pubsub/mqtt/mock_client.go b/pkg/gofr/datasource/pubsub/mqtt/mock_client.go new file mode 100644 index 000000000..ef0079b86 --- /dev/null +++ b/pkg/gofr/datasource/pubsub/mqtt/mock_client.go @@ -0,0 +1,180 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/eclipse/paho.mqtt.golang (interfaces: Client) +// +// Generated by this command: +// +// mockgen -destination=mock_client.go -package=mqtt github.com/eclipse/paho.mqtt.golang Client +// + +// Package mqtt is a generated GoMock package. +package mqtt + +import ( + reflect "reflect" + + mqtt "github.com/eclipse/paho.mqtt.golang" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// AddRoute mocks base method. +func (m *MockClient) AddRoute(arg0 string, arg1 mqtt.MessageHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddRoute", arg0, arg1) +} + +// AddRoute indicates an expected call of AddRoute. +func (mr *MockClientMockRecorder) AddRoute(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRoute", reflect.TypeOf((*MockClient)(nil).AddRoute), arg0, arg1) +} + +// Connect mocks base method. +func (m *MockClient) Connect() mqtt.Token { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Connect") + ret0, _ := ret[0].(mqtt.Token) + return ret0 +} + +// Connect indicates an expected call of Connect. +func (mr *MockClientMockRecorder) Connect() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockClient)(nil).Connect)) +} + +// Disconnect mocks base method. +func (m *MockClient) Disconnect(arg0 uint) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Disconnect", arg0) +} + +// Disconnect indicates an expected call of Disconnect. +func (mr *MockClientMockRecorder) Disconnect(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockClient)(nil).Disconnect), arg0) +} + +// IsConnected mocks base method. +func (m *MockClient) IsConnected() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsConnected") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsConnected indicates an expected call of IsConnected. +func (mr *MockClientMockRecorder) IsConnected() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockClient)(nil).IsConnected)) +} + +// IsConnectionOpen mocks base method. +func (m *MockClient) IsConnectionOpen() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsConnectionOpen") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsConnectionOpen indicates an expected call of IsConnectionOpen. +func (mr *MockClientMockRecorder) IsConnectionOpen() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnectionOpen", reflect.TypeOf((*MockClient)(nil).IsConnectionOpen)) +} + +// OptionsReader mocks base method. +func (m *MockClient) OptionsReader() mqtt.ClientOptionsReader { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OptionsReader") + ret0, _ := ret[0].(mqtt.ClientOptionsReader) + return ret0 +} + +// OptionsReader indicates an expected call of OptionsReader. +func (mr *MockClientMockRecorder) OptionsReader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OptionsReader", reflect.TypeOf((*MockClient)(nil).OptionsReader)) +} + +// Publish mocks base method. +func (m *MockClient) Publish(arg0 string, arg1 byte, arg2 bool, arg3 any) mqtt.Token { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Publish", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(mqtt.Token) + return ret0 +} + +// Publish indicates an expected call of Publish. +func (mr *MockClientMockRecorder) Publish(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockClient)(nil).Publish), arg0, arg1, arg2, arg3) +} + +// Subscribe mocks base method. +func (m *MockClient) Subscribe(arg0 string, arg1 byte, arg2 mqtt.MessageHandler) mqtt.Token { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Subscribe", arg0, arg1, arg2) + ret0, _ := ret[0].(mqtt.Token) + return ret0 +} + +// Subscribe indicates an expected call of Subscribe. +func (mr *MockClientMockRecorder) Subscribe(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockClient)(nil).Subscribe), arg0, arg1, arg2) +} + +// SubscribeMultiple mocks base method. +func (m *MockClient) SubscribeMultiple(arg0 map[string]byte, arg1 mqtt.MessageHandler) mqtt.Token { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeMultiple", arg0, arg1) + ret0, _ := ret[0].(mqtt.Token) + return ret0 +} + +// SubscribeMultiple indicates an expected call of SubscribeMultiple. +func (mr *MockClientMockRecorder) SubscribeMultiple(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeMultiple", reflect.TypeOf((*MockClient)(nil).SubscribeMultiple), arg0, arg1) +} + +// Unsubscribe mocks base method. +func (m *MockClient) Unsubscribe(arg0 ...string) mqtt.Token { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Unsubscribe", varargs...) + ret0, _ := ret[0].(mqtt.Token) + return ret0 +} + +// Unsubscribe indicates an expected call of Unsubscribe. +func (mr *MockClientMockRecorder) Unsubscribe(arg0 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockClient)(nil).Unsubscribe), arg0...) +} diff --git a/pkg/gofr/datasource/pubsub/mqtt/mock_interfaces.go b/pkg/gofr/datasource/pubsub/mqtt/mock_interfaces.go index 42840bc89..7c2e5c8d3 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/mock_interfaces.go +++ b/pkg/gofr/datasource/pubsub/mqtt/mock_interfaces.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: interface.go +// +// Generated by this command: +// +// mockgen -source=interface.go -destination=mock_interfaces.go -package=mqtt +// // Package mqtt is a generated GoMock package. package mqtt @@ -9,8 +14,117 @@ import ( reflect "reflect" gomock "go.uber.org/mock/gomock" + + datasource "gofr.dev/pkg/gofr/datasource" ) +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debug mocks base method. +func (m *MockLogger) Debug(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(format string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(format any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(format string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(format any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Infof mocks base method. +func (m *MockLogger) Infof(format string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Infof", varargs...) +} + +// Infof indicates an expected call of Infof. +func (mr *MockLoggerMockRecorder) Infof(format any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...) +} + +// Warnf mocks base method. +func (m *MockLogger) Warnf(format string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warnf", varargs...) +} + +// Warnf indicates an expected call of Warnf. +func (mr *MockLoggerMockRecorder) Warnf(format any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...) +} + // MockMetrics is a mock of Metrics interface. type MockMetrics struct { ctrl *gomock.Controller @@ -37,7 +151,7 @@ func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { // IncrementCounter mocks base method. func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) { m.ctrl.T.Helper() - varargs := []interface{}{ctx, name} + varargs := []any{ctx, name} for _, a := range labels { varargs = append(varargs, a) } @@ -45,8 +159,113 @@ func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels } // IncrementCounter indicates an expected call of IncrementCounter. -func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name interface{}, labels ...interface{}) *gomock.Call { +func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, name}, labels...) + varargs := append([]any{ctx, name}, labels...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...) } + +// MockPubSub is a mock of PubSub interface. +type MockPubSub struct { + ctrl *gomock.Controller + recorder *MockPubSubMockRecorder +} + +// MockPubSubMockRecorder is the mock recorder for MockPubSub. +type MockPubSubMockRecorder struct { + mock *MockPubSub +} + +// NewMockPubSub creates a new mock instance. +func NewMockPubSub(ctrl *gomock.Controller) *MockPubSub { + mock := &MockPubSub{ctrl: ctrl} + mock.recorder = &MockPubSubMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPubSub) EXPECT() *MockPubSubMockRecorder { + return m.recorder +} + +// Disconnect mocks base method. +func (m *MockPubSub) Disconnect(waitTime uint) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Disconnect", waitTime) +} + +// Disconnect indicates an expected call of Disconnect. +func (mr *MockPubSubMockRecorder) Disconnect(waitTime any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockPubSub)(nil).Disconnect), waitTime) +} + +// Health mocks base method. +func (m *MockPubSub) Health() datasource.Health { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Health") + ret0, _ := ret[0].(datasource.Health) + return ret0 +} + +// Health indicates an expected call of Health. +func (mr *MockPubSubMockRecorder) Health() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockPubSub)(nil).Health)) +} + +// Ping mocks base method. +func (m *MockPubSub) Ping() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ping") + ret0, _ := ret[0].(error) + return ret0 +} + +// Ping indicates an expected call of Ping. +func (mr *MockPubSubMockRecorder) Ping() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockPubSub)(nil).Ping)) +} + +// Publish mocks base method. +func (m *MockPubSub) Publish(ctx context.Context, topic string, message []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Publish", ctx, topic, message) + ret0, _ := ret[0].(error) + return ret0 +} + +// Publish indicates an expected call of Publish. +func (mr *MockPubSubMockRecorder) Publish(ctx, topic, message any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockPubSub)(nil).Publish), ctx, topic, message) +} + +// SubscribeWithFunction mocks base method. +func (m *MockPubSub) SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeWithFunction", topic, subscribeFunc) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubscribeWithFunction indicates an expected call of SubscribeWithFunction. +func (mr *MockPubSubMockRecorder) SubscribeWithFunction(topic, subscribeFunc any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeWithFunction", reflect.TypeOf((*MockPubSub)(nil).SubscribeWithFunction), topic, subscribeFunc) +} + +// Unsubscribe mocks base method. +func (m *MockPubSub) Unsubscribe(topic string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unsubscribe", topic) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unsubscribe indicates an expected call of Unsubscribe. +func (mr *MockPubSubMockRecorder) Unsubscribe(topic any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockPubSub)(nil).Unsubscribe), topic) +} diff --git a/pkg/gofr/datasource/pubsub/mqtt/mock_token.go b/pkg/gofr/datasource/pubsub/mqtt/mock_token.go new file mode 100644 index 000000000..136ddfd35 --- /dev/null +++ b/pkg/gofr/datasource/pubsub/mqtt/mock_token.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/eclipse/paho.mqtt.golang (interfaces: Token) +// +// Generated by this command: +// +// mockgen -destination=mock_token.go -package=mqtt github.com/eclipse/paho.mqtt.golang Token +// + +// Package mqtt is a generated GoMock package. +package mqtt + +import ( + reflect "reflect" + time "time" + + gomock "go.uber.org/mock/gomock" +) + +// MockToken is a mock of Token interface. +type MockToken struct { + ctrl *gomock.Controller + recorder *MockTokenMockRecorder +} + +// MockTokenMockRecorder is the mock recorder for MockToken. +type MockTokenMockRecorder struct { + mock *MockToken +} + +// NewMockToken creates a new mock instance. +func NewMockToken(ctrl *gomock.Controller) *MockToken { + mock := &MockToken{ctrl: ctrl} + mock.recorder = &MockTokenMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockToken) EXPECT() *MockTokenMockRecorder { + return m.recorder +} + +// Done mocks base method. +func (m *MockToken) Done() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Done") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// Done indicates an expected call of Done. +func (mr *MockTokenMockRecorder) Done() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Done", reflect.TypeOf((*MockToken)(nil).Done)) +} + +// Error mocks base method. +func (m *MockToken) Error() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Error") + ret0, _ := ret[0].(error) + return ret0 +} + +// Error indicates an expected call of Error. +func (mr *MockTokenMockRecorder) Error() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockToken)(nil).Error)) +} + +// Wait mocks base method. +func (m *MockToken) Wait() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Wait") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Wait indicates an expected call of Wait. +func (mr *MockTokenMockRecorder) Wait() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockToken)(nil).Wait)) +} + +// WaitTimeout mocks base method. +func (m *MockToken) WaitTimeout(arg0 time.Duration) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitTimeout", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// WaitTimeout indicates an expected call of WaitTimeout. +func (mr *MockTokenMockRecorder) WaitTimeout(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitTimeout", reflect.TypeOf((*MockToken)(nil).WaitTimeout), arg0) +} diff --git a/pkg/gofr/datasource/pubsub/mqtt/mqtt.go b/pkg/gofr/datasource/pubsub/mqtt/mqtt.go index cd6b50730..f826ebfe0 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/mqtt.go +++ b/pkg/gofr/datasource/pubsub/mqtt/mqtt.go @@ -34,10 +34,9 @@ type MQTT struct { logger Logger metrics Metrics - config *Config - msgChanMap map[string]chan *pubsub.Message - - mu *sync.RWMutex + config *Config + subscriptions map[string]subscription + mu *sync.RWMutex } type Config struct { @@ -50,6 +49,12 @@ type Config struct { QoS byte Order bool RetrieveRetained bool + KeepAlive time.Duration +} + +type subscription struct { + msgs chan *pubsub.Message + handler func(_ mqtt.Client, msg mqtt.Message) } // New establishes a connection to MQTT Broker using the configs and return pubsub.MqttPublisherSubscriber @@ -60,23 +65,25 @@ func New(config *Config, logger Logger, metrics Metrics) *MQTT { } options := getMQTTClientOptions(config) + subs := make(map[string]subscription) + mu := new(sync.RWMutex) logger.Debugf("connecting to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, config.ClientID) + options.SetOnConnectHandler(createReconnectHandler(mu, config, subs)) + options.SetConnectionLostHandler(createConnectionLostHandler(logger)) + options.SetReconnectingHandler(createReconnectingHandler(logger, config)) // create the client using the options above client := mqtt.NewClient(options) - if token := client.Connect(); token.Wait() && token.Error() != nil { logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error()) return &MQTT{Client: client, config: config, logger: logger} } - msg := make(map[string]chan *pubsub.Message) - logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, options.ClientID) - return &MQTT{Client: client, config: config, logger: logger, msgChanMap: msg, mu: new(sync.RWMutex), metrics: metrics} + return &MQTT{Client: client, config: config, logger: logger, subscriptions: subs, mu: mu, metrics: metrics} } func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT { @@ -89,6 +96,8 @@ func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT { opts := mqtt.NewClientOptions() opts.AddBroker(fmt.Sprintf("tcp://%s:%d", host, port)) opts.SetClientID(clientID) + opts.SetAutoReconnect(true) + opts.SetKeepAlive(config.KeepAlive) client := mqtt.NewClient(opts) if token := client.Connect(); token.Wait() && token.Error() != nil { @@ -101,11 +110,11 @@ func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT { config.Port = port config.ClientID = clientID - msg := make(map[string]chan *pubsub.Message) + msg := make(map[string]subscription) logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, clientID) - return &MQTT{Client: client, config: config, logger: logger, msgChanMap: msg, mu: new(sync.RWMutex), metrics: metrics} + return &MQTT{Client: client, config: config, logger: logger, subscriptions: msg, mu: new(sync.RWMutex), metrics: metrics} } func getMQTTClientOptions(config *Config) *mqtt.ClientOptions { @@ -125,6 +134,8 @@ func getMQTTClientOptions(config *Config) *mqtt.ClientOptions { options.SetOrderMatters(config.Order) options.SetResumeSubs(config.RetrieveRetained) + options.SetAutoReconnect(true) + options.SetKeepAlive(config.KeepAlive) return options } @@ -143,23 +154,40 @@ func getClientID(clientID string) string { } func (m *MQTT) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) { - ctx, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "mqtt-subscribe") - defer span.End() - - m.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic) - - var messg = pubsub.NewMessage(ctx) - m.mu.Lock() // get the message channel for the given topic - msgChan, ok := m.msgChanMap[topic] + subs, ok := m.subscriptions[topic] if !ok { - msgChan = make(chan *pubsub.Message, messageBuffer) - m.msgChanMap[topic] = msgChan + subs.msgs = make(chan *pubsub.Message, messageBuffer) + subs.handler = m.createMqttHandler(ctx, topic, subs.msgs) + token := m.Client.Subscribe(topic, m.config.QoS, subs.handler) + + if token.Wait() && token.Error() != nil { + m.logger.Errorf("error getting a message from MQTT, error: %v", token.Error()) + return nil, token.Error() + } + + m.subscriptions[topic] = subs } m.mu.Unlock() - handler := func(_ mqtt.Client, msg mqtt.Message) { + // blocks if there are no messages in the channel + msg := <-subs.msgs + m.metrics.IncrementCounter(msg.Context(), "app_pubsub_subscribe_success_count", "topic", msg.Topic) + + return msg, nil +} + +func (m *MQTT) createMqttHandler(_ context.Context, topic string, msgs chan *pubsub.Message) mqtt.MessageHandler { + return func(_ mqtt.Client, msg mqtt.Message) { + ctx := context.Background() + ctx, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "mqtt-subscribe") + + defer span.End() + + m.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic) + + var messg = pubsub.NewMessage(context.WithoutCancel(ctx)) messg.Topic = msg.Topic() messg.Value = msg.Payload() messg.MetaData = map[string]string{ @@ -171,7 +199,7 @@ func (m *MQTT) Subscribe(ctx context.Context, topic string) (*pubsub.Message, er messg.Committer = &message{msg: msg} // store the message in the channel - msgChan <- messg + msgs <- messg m.logger.Debug(&pubsub.Log{ Mode: "SUB", @@ -182,21 +210,7 @@ func (m *MQTT) Subscribe(ctx context.Context, topic string) (*pubsub.Message, er PubSubBackend: "MQTT", }) } - - token := m.Client.Subscribe(topic, m.config.QoS, handler) - - if token.Wait() && token.Error() != nil { - m.logger.Errorf("error getting a message from MQTT, error: %v", token.Error()) - - return nil, token.Error() - } - - m.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic) - - // blocks if there are no messages in the channel - return <-msgChan, nil } - func (m *MQTT) Publish(ctx context.Context, topic string, message []byte) error { _, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "mqtt-publish") defer span.End() @@ -282,7 +296,17 @@ func (m *MQTT) DeleteTopic(_ context.Context, _ string) error { // SubscribeWithFunction subscribe with a subscribing function, called whenever broker publishes a message. func (m *MQTT) SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) error { - handler := func(_ mqtt.Client, msg mqtt.Message) { + token := m.Client.Subscribe(topic, 1, getHandler(subscribeFunc)) + + if token.Wait() && token.Error() != nil { + return token.Error() + } + + return nil +} + +func getHandler(subscribeFunc SubscribeFunc) func(client mqtt.Client, msg mqtt.Message) { + return func(_ mqtt.Client, msg mqtt.Message) { pubsubMsg := &pubsub.Message{ Topic: msg.Topic(), Value: msg.Payload(), @@ -294,22 +318,12 @@ func (m *MQTT) SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) } // call the user defined function - err := subscribeFunc(pubsubMsg) - if err != nil { - return - } + _ = subscribeFunc(pubsubMsg) } - - token := m.Client.Subscribe(topic, 1, handler) - - if token.Wait() && token.Error() != nil { - return token.Error() - } - - return nil } func (m *MQTT) Unsubscribe(topic string) error { + m.mu.RLock() token := m.Client.Unsubscribe(topic) token.Wait() @@ -319,11 +333,24 @@ func (m *MQTT) Unsubscribe(topic string) error { return token.Error() } + sub, ok := m.subscriptions[topic] + if ok { + close(sub.msgs) + delete(m.subscriptions, topic) + } + m.mu.RUnlock() + return nil } func (m *MQTT) Disconnect(waitTime uint) { + m.mu.RLock() + for topic := range m.subscriptions { + _ = m.Unsubscribe(topic) + } + m.Client.Disconnect(waitTime) + m.mu.RUnlock() } func (m *MQTT) Ping() error { @@ -335,3 +362,25 @@ func (m *MQTT) Ping() error { return nil } + +func createReconnectHandler(mu *sync.RWMutex, config *Config, subs map[string]subscription) func(c mqtt.Client) { + return func(c mqtt.Client) { + mu.RLock() + for k, v := range subs { + c.Subscribe(k, config.QoS, v.handler) + } + mu.RUnlock() + } +} + +func createConnectionLostHandler(logger Logger) func(_ mqtt.Client, err error) { + return func(_ mqtt.Client, err error) { + logger.Errorf("mqtt connection lost, error: %v", err.Error()) + } +} + +func createReconnectingHandler(logger Logger, config *Config) func(mqtt.Client, *mqtt.ClientOptions) { + return func(_ mqtt.Client, _ *mqtt.ClientOptions) { + logger.Infof("reconnecting to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, config.ClientID) + } +} diff --git a/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go b/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go index b60ab1cd4..adab7f2e9 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go +++ b/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -16,7 +17,28 @@ import ( "gofr.dev/pkg/gofr/testutil" ) -var errTest = errors.New("test error") +var ( + errToken = errors.New("connection error with MQTT") + errTest = errors.New("test error") +) + +var ( + //nolint:gochecknoglobals //used for testing purposes only + mockConfigs = &Config{ + Protocol: "tcp", + Hostname: "localhost", + Port: 1883, + Username: "admin", + Password: "admin", + ClientID: "abc2222", + QoS: 1, + Order: false, + RetrieveRetained: false, + KeepAlive: 0, + } + //nolint:gochecknoglobals //used for testing purposes only + msg = []byte(`hello world`) +) func TestMQTT_New(t *testing.T) { var client *MQTT @@ -40,7 +62,7 @@ func TestMQTT_New(t *testing.T) { } // TestMQTT_EmptyConfigs test the scenario where configs are not provided and -// client tries to connect to the public broker. +// a client tries to connect to the public broker. func TestMQTT_EmptyConfigs(t *testing.T) { var client *MQTT @@ -76,56 +98,92 @@ func TestMQTT_getMQTTClientOptions(t *testing.T) { } func TestMQTT_Ping(t *testing.T) { - m := New(&Config{}, logging.NewMockLogger(logging.FATAL), nil) + ctrl, mq, mockClient, _, _ := getMockMQTT(t, nil) + defer ctrl.Finish() + mockClient.EXPECT().IsConnected().Return(true) // Success Case - err := m.Ping() + err := mq.Ping() assert.Nil(t, err) + mockClient.EXPECT().Disconnect(uint(1)) // Disconnect the client - m.Disconnect(1) + mq.Disconnect(1) + mockClient.EXPECT().IsConnected().Return(false) // Failure Case - err = m.Ping() + err = mq.Ping() assert.NotNil(t, err) assert.Equal(t, errClientNotConnected, err) } func TestMQTT_Disconnect(t *testing.T) { - ctrl := gomock.NewController(t) + ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs) defer ctrl.Finish() ctx := context.TODO() - mockMetrics := NewMockMetrics(ctrl) - - mockLogger := logging.NewMockLogger(logging.ERROR) - client := New(&Config{}, mockLogger, mockMetrics) mockMetrics.EXPECT(). IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test") + mockClient.EXPECT().Disconnect(uint(1)) + + mockClient.EXPECT().Publish("test", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg).Return(mockToken) + + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(3) + // Disconnect the broker and then try to publish client.Disconnect(1) - err := client.Publish(ctx, "test", []byte("hello")) + err := client.Publish(ctx, "test", msg) assert.NotNil(t, err) - assert.Equal(t, "not Connected", err.Error()) + assert.True(t, errors.Is(err, errToken)) } -func TestMQTT_PublishSuccess(t *testing.T) { - ctrl := gomock.NewController(t) +func TestMQTT_DisconnectWithSubscriptions(t *testing.T) { + subs := make(map[string]subscription) + subs["test/topic"] = subscription{ + msgs: make(chan *pubsub.Message), + handler: func(_ mqtt.Client, _ mqtt.Message) {}, + } + + ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs) defer ctrl.Finish() - ctx := context.TODO() - mockMetrics := NewMockMetrics(ctrl) - mockMetrics.EXPECT(). - IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic") - mockMetrics.EXPECT(). - IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", "test/topic") + client.subscriptions = subs + + mockClient.EXPECT().Disconnect(uint(1)) + mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + client.Disconnect(1) + + // we assert that on unsubscribing the subscription gets deleted + _, ok := client.subscriptions["test/topic"] + assert.False(t, ok) +} +func TestMQTT_PublishSuccess(t *testing.T) { out := testutil.StdoutOutputForFunc(func() { - m := New(&Config{}, logging.NewMockLogger(logging.DEBUG), mockMetrics) - err := m.Publish(ctx, "test/topic", []byte(`hello world`)) + ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() + + ctx := context.TODO() + + mockMetrics.EXPECT(). + IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic") + mockMetrics.EXPECT(). + IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", "test/topic") + + mockClient.EXPECT().Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg). + Return(mockToken) + + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + err := client.Publish(ctx, "test/topic", msg) assert.Nil(t, err) }) @@ -137,98 +195,80 @@ func TestMQTT_PublishSuccess(t *testing.T) { } func TestMQTT_PublishFailure(t *testing.T) { - ctrl := gomock.NewController(t) + ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs) defer ctrl.Finish() ctx := context.TODO() - mockMetrics := NewMockMetrics(ctrl) - out := testutil.StderrOutputForFunc(func() { - mockLogger := logging.NewMockLogger(logging.ERROR) - - // case where the client has been disconnected, resulting in a Publishing failure - mockMetrics.EXPECT(). - IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic") - - m := New(&Config{}, mockLogger, mockMetrics) + // case where the client has been disconnected, resulting in a Publishing failure + mockMetrics.EXPECT(). + IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic") - // Disconnect the client - m.Client.Disconnect(1) + mockClient.EXPECT().Disconnect(uint(1)) + // Disconnect the client + client.Disconnect(1) - err := m.Publish(ctx, "test/topic", []byte(`hello world`)) + mockClient.EXPECT().Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg).Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(3) - assert.NotNil(t, err) - }) + err := client.Publish(ctx, "test/topic", []byte(`hello world`)) - assert.Contains(t, out, "error while publishing") + assert.NotNil(t, err) + assert.True(t, errors.Is(err, errToken)) } func TestMQTT_SubscribeSuccess(t *testing.T) { - ctrl := gomock.NewController(t) + ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs) defer ctrl.Finish() ctx := context.TODO() - mockMetrics := NewMockMetrics(ctrl) - mockLogger := logging.NewMockLogger(logging.ERROR) - // expect the publishing metric calls - mockMetrics.EXPECT(). - IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "topic", "test/topic") - mockMetrics.EXPECT(). - IncrementCounter(gomock.Any(), "app_pubsub_publish_success_count", "topic", "test/topic") - - // expect the subcscibers metric calls - mockMetrics.EXPECT(). - IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test/topic") mockMetrics.EXPECT(). IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", "test/topic") - m := New(&Config{QoS: 0}, mockLogger, mockMetrics) - wg := sync.WaitGroup{} + mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken) - wg.Add(1) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) go func() { - defer wg.Done() - - msg, err := m.Subscribe(ctx, "test/topic") - - assert.NotNil(t, msg) - assert.Equal(t, "test/topic", msg.Topic) - - assert.Nil(t, err) + client.subscriptions["test/topic"].msgs <- &pubsub.Message{ + Topic: "test/topic", + Value: msg, + MetaData: nil, + Committer: &message{msg: mockMessage{}}, + } }() - _ = m.Publish(ctx, "test/topic", []byte("hello world")) + m, err := client.Subscribe(ctx, "test/topic") - wg.Wait() + assert.Equal(t, msg, m.Value) + assert.Nil(t, err) } func TestMQTT_SubscribeFailure(t *testing.T) { - // Subscribing on a disconncted client - ctrl := gomock.NewController(t) + ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs) defer ctrl.Finish() ctx := context.TODO() - mockMetrics := NewMockMetrics(ctrl) - mockLogger := logging.NewMockLogger(logging.ERROR) - // expect the subcscibers metric calls - mockMetrics.EXPECT(). - IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test/topic") + mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken) - m := New(&Config{QoS: 0}, mockLogger, mockMetrics) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(3) - // Disconnect the client - m.Client.Disconnect(1) - msg, err := m.Subscribe(ctx, "test/topic") + m, err := client.Subscribe(ctx, "test/topic") + assert.Nil(t, m) assert.NotNil(t, err) - assert.Nil(t, msg) + assert.True(t, errors.Is(err, errToken)) } func TestMQTT_SubscribeWithFunc(t *testing.T) { + ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() + subscriptionFunc := func(msg *pubsub.Message) error { - assert.NotNil(t, msg) assert.Equal(t, "test/topic", msg.Topic) return nil @@ -238,37 +278,85 @@ func TestMQTT_SubscribeWithFunc(t *testing.T) { return errTest } - m := New(&Config{}, logging.NewMockLogger(logging.ERROR), nil) - // Success case - err := m.SubscribeWithFunction("test/topic", subscriptionFunc) + mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + err := client.SubscribeWithFunction("test/topic", subscriptionFunc) assert.Nil(t, err) // Error case where error is returned from subscription function - err = m.SubscribeWithFunction("test/topic", subscriptionFuncErr) + mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + err = client.SubscribeWithFunction("test/topic", subscriptionFuncErr) assert.Nil(t, err) // Unsubscribe from the topic - _ = m.Unsubscribe("test/topic") + mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + _ = client.Unsubscribe("test/topic") // Error case where the client cannot connect - m.Disconnect(1) - err = m.SubscribeWithFunction("test/topic", subscriptionFunc) + mockClient.EXPECT().Disconnect(uint(1)) + client.Disconnect(1) + + mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(2) + + err = client.SubscribeWithFunction("test/topic", subscriptionFunc) assert.NotNil(t, err) + assert.True(t, errors.Is(err, errToken)) +} + +func Test_getHandler(t *testing.T) { + subscriptionFunc := func(msg *pubsub.Message) error { + assert.Equal(t, []byte("hello from sub func"), msg.Value) + assert.Equal(t, map[string]string{"qos": string(byte(1)), "retained": "false", "messageID": "123"}, msg.MetaData) + assert.Equal(t, "test/topic", msg.Topic) + + return nil + } + + h := getHandler(subscriptionFunc) + + h(nil, mockMessage{ + duplicate: false, + qos: 1, + retained: false, + topic: "test/topic", + messageID: 123, + pyload: "hello from sub func", + }) } func TestMQTT_Unsubscribe(t *testing.T) { out := testutil.StderrOutputForFunc(func() { - mockLogger := logging.NewMockLogger(logging.ERROR) - m := New(&Config{}, mockLogger, nil) + ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() // Success case - err := m.Unsubscribe("test/topic") + mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + err := client.Unsubscribe("test/topic") assert.Nil(t, err) // Failure case - m.Client.Disconnect(1) - err = m.Unsubscribe("test/topic") + mockClient.EXPECT().Disconnect(uint(1)) + client.Disconnect(1) + + mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(3) + + err = client.Unsubscribe("test/topic") assert.NotNil(t, err) }) @@ -277,16 +365,30 @@ func TestMQTT_Unsubscribe(t *testing.T) { func TestMQTT_CreateTopic(t *testing.T) { out := testutil.StderrOutputForFunc(func() { - mockLogger := logging.NewMockLogger(logging.ERROR) - m := New(&Config{}, mockLogger, nil) + ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() // Success case - err := m.CreateTopic(context.TODO(), "test/topic") + mockClient.EXPECT(). + Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, []byte("topic creation")). + Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(nil) + + err := client.CreateTopic(context.TODO(), "test/topic") assert.Nil(t, err) // Failure case - m.Client.Disconnect(1) - err = m.CreateTopic(context.TODO(), "test/topic") + mockClient.EXPECT().Disconnect(uint(1)) + client.Client.Disconnect(1) + + mockClient.EXPECT(). + Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, []byte("topic creation")). + Return(mockToken) + mockToken.EXPECT().Wait().Return(true) + mockToken.EXPECT().Error().Return(errToken).Times(3) + + err = client.CreateTopic(context.TODO(), "test/topic") assert.NotNil(t, err) }) @@ -308,14 +410,15 @@ func TestMQTT_Health(t *testing.T) { // The client ping fails out = testutil.StderrOutputForFunc(func() { - m := New(&Config{}, logging.NewMockLogger(logging.ERROR), nil) + ctrl, client, mockClient, _, _ := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() - m.Disconnect(1) + mockClient.EXPECT().IsConnected().Return(false) - res := m.Health() + res := client.Health() assert.Equal(t, datasource.Health{ Status: "DOWN", - Details: map[string]interface{}{"backend": "MQTT", "host": publicBroker}, + Details: map[string]interface{}{"backend": "MQTT", "host": "localhost"}, }, res) }) @@ -323,17 +426,138 @@ func TestMQTT_Health(t *testing.T) { // Success Case - Status UP _ = testutil.StderrOutputForFunc(func() { - m := New(&Config{}, logging.NewMockLogger(logging.ERROR), nil) - res := m.Health() + ctrl, client, mockClient, _, _ := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() + + mockClient.EXPECT().IsConnected().Return(true) + + res := client.Health() assert.Equal(t, datasource.Health{ Status: "UP", - Details: map[string]interface{}{"backend": "MQTT", "host": publicBroker}, + Details: map[string]interface{}{"backend": "MQTT", "host": "localhost"}, }, res) }) } -// Non-functional test case; for coverage purposes. -func TestMQTT_DeleteTopic(_ *testing.T) { +func TestMQTT_DeleteTopic(t *testing.T) { m := &MQTT{} - _ = m.DeleteTopic(context.TODO(), "") + + err := m.DeleteTopic(context.TODO(), "test/topic") + assert.Nil(t, err) +} + +func TestReconnectingHandler(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockLogger.EXPECT().Infof("reconnecting to MQTT at '%v:%v' with clientID '%v'", "any", 1234, "gopher") + + handler := createReconnectingHandler(mockLogger, &Config{ + Hostname: "any", + Port: 1234, + ClientID: "gopher", + }) + assert.NotNil(t, handler) + + handler(NewMockClient(ctrl), &mqtt.ClientOptions{}) +} + +func TestConnectionLostHandler(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockLogger.EXPECT().Errorf("mqtt connection lost, error: %v", gomock.Any()). + DoAndReturn(func(_ string, args ...interface{}) { + assert.Len(t, args, 1) + assert.Error(t, mqtt.ErrNotConnected, args[0]) + }) + + connectionLostHandler := createConnectionLostHandler(mockLogger) + connectionLostHandler(NewMockClient(ctrl), mqtt.ErrNotConnected) +} + +func TestReconnectHandler(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + qos := byte(1) + + clientMock := NewMockClient(ctrl) + clientMock.EXPECT().Subscribe("topic1", qos, gomock.Any()).Return(nil) + + msgsChan := make(chan *pubsub.Message) + + defer close(msgsChan) + + subs := map[string]subscription{ + "topic1": { + msgs: msgsChan, + handler: func(_ mqtt.Client, _ mqtt.Message) {}, + }, + } + + reconnectHandler := createReconnectHandler(&sync.RWMutex{}, &Config{ + Hostname: "any", + Port: 1234, + ClientID: "gopher", + QoS: qos, + }, subs) + + assert.NotNil(t, reconnectHandler) + + reconnectHandler(clientMock) +} + +func TestMQTT_createMqttHandler(t *testing.T) { + var ( + out string + msgs = make(chan *pubsub.Message) + wg = sync.WaitGroup{} + ) + + wg.Add(1) + + go func() { + defer wg.Done() + + out = testutil.StdoutOutputForFunc(func() { + ctrl, client, _, mockMetrics, _ := getMockMQTT(t, mockConfigs) + defer ctrl.Finish() + + mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test/topic") + + handler := client.createMqttHandler(context.TODO(), "test/topic", msgs) + + handler(nil, mockMessage{false, 1, false, "test/topic", 123, "hello world"}) + }) + }() + + m := <-msgs + close(msgs) + + assert.NotNil(t, m) + assert.Equal(t, m.Value, msg) + + // wait for the goroutine test to finish writing log to out + wg.Wait() + assert.Contains(t, out, "SUB") + assert.Contains(t, out, "hello world") + assert.Contains(t, out, "test/topic") + assert.Contains(t, out, "MQTT") +} + +func getMockMQTT(t *testing.T, conf *Config) (*gomock.Controller, *MQTT, *MockClient, *MockMetrics, *MockToken) { + t.Helper() + + ctrl := gomock.NewController(t) + mockClient := NewMockClient(ctrl) + mockToken := NewMockToken(ctrl) + mockLogger := logging.NewMockLogger(logging.DEBUG) + mockMetrics := NewMockMetrics(ctrl) + + mq := &MQTT{mockClient, mockLogger, mockMetrics, conf, make(map[string]subscription), &sync.RWMutex{}} + + return ctrl, mq, mockClient, mockMetrics, mockToken } diff --git a/pkg/gofr/datasource/redis/hook.go b/pkg/gofr/datasource/redis/hook.go index 78b68cc54..e373e62b2 100644 --- a/pkg/gofr/datasource/redis/hook.go +++ b/pkg/gofr/datasource/redis/hook.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "gofr.dev/pkg/gofr/datasource" - "github.com/redis/go-redis/v9" + + "gofr.dev/pkg/gofr/datasource" ) // redisHook is a custom Redis hook for logging queries and their durations. diff --git a/pkg/gofr/datasource/redis/redis_test.go b/pkg/gofr/datasource/redis/redis_test.go index 40d22c937..daa349022 100644 --- a/pkg/gofr/datasource/redis/redis_test.go +++ b/pkg/gofr/datasource/redis/redis_test.go @@ -7,7 +7,6 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr/config" diff --git a/pkg/gofr/datasource/sql/db_test.go b/pkg/gofr/datasource/sql/db_test.go index 8a5c46802..3c4db7435 100644 --- a/pkg/gofr/datasource/sql/db_test.go +++ b/pkg/gofr/datasource/sql/db_test.go @@ -22,6 +22,8 @@ var ( ) func getDB(t *testing.T, logLevel logging.Level) (*DB, sqlmock.Sqlmock) { + t.Helper() + mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), sqlmock.MonitorPingsOption(true)) if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) diff --git a/pkg/gofr/datasource/sql/sql.go b/pkg/gofr/datasource/sql/sql.go index 5afeafe53..4f7659427 100644 --- a/pkg/gofr/datasource/sql/sql.go +++ b/pkg/gofr/datasource/sql/sql.go @@ -113,17 +113,18 @@ func retryConnection(database *DB) { database.logger.Log("retrying SQL database connection") for { - if err := database.DB.Ping(); err != nil { - database.logger.Debugf("could not connect with '%s' user to '%s' database at '%s:%s', error: %v", - database.config.User, database.config.Database, database.config.HostName, database.config.Port, err) - - time.Sleep(connRetryFrequencyInSeconds * time.Second) - } else { + err := database.DB.Ping() + if err == nil { database.logger.Logf("connected to '%s' database at '%s:%s'", database.config.Database, database.config.HostName, database.config.Port) break } + + database.logger.Debugf("could not connect with '%s' user to '%s' database at '%s:%s', error: %v", + database.config.User, database.config.Database, database.config.HostName, database.config.Port, err) + + time.Sleep(connRetryFrequencyInSeconds * time.Second) } } diff --git a/pkg/gofr/datasource/sql/sql_mock.go b/pkg/gofr/datasource/sql/sql_mock.go index f6ca0d82b..fc9b3bde6 100644 --- a/pkg/gofr/datasource/sql/sql_mock.go +++ b/pkg/gofr/datasource/sql/sql_mock.go @@ -10,10 +10,14 @@ import ( ) func NewSQLMocks(t *testing.T) (*DB, sqlmock.Sqlmock, *MockMetrics) { + t.Helper() + return NewSQLMocksWithConfig(t, nil) } func NewSQLMocksWithConfig(t *testing.T, config *DBConfig) (*DB, sqlmock.Sqlmock, *MockMetrics) { + t.Helper() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) diff --git a/pkg/gofr/exporter.go b/pkg/gofr/exporter.go index 8d61ca7d6..73558c3b8 100644 --- a/pkg/gofr/exporter.go +++ b/pkg/gofr/exporter.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" + "gofr.dev/pkg/gofr/logging" ) @@ -67,7 +68,7 @@ func (e *Exporter) processSpans(ctx context.Context, logger logging.Logger, span return fmt.Errorf("failed to marshal spans, error: %w", err) } - req, err := http.NewRequestWithContext(ctx, "POST", e.endpoint, bytes.NewBuffer(payload)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.endpoint, bytes.NewBuffer(payload)) if err != nil { return fmt.Errorf("failed to create HTTP request, error: %w", err) } diff --git a/pkg/gofr/exporter_test.go b/pkg/gofr/exporter_test.go index 497cf8fca..93958947e 100644 --- a/pkg/gofr/exporter_test.go +++ b/pkg/gofr/exporter_test.go @@ -50,6 +50,8 @@ func Test_ExportSpansError(t *testing.T) { } func provideSampleSpan(t *testing.T) []sdktrace.ReadOnlySpan { + t.Helper() + tp := sdktrace.NewTracerProvider() defer func(tp *sdktrace.TracerProvider, ctx context.Context) { diff --git a/pkg/gofr/externalDB.go b/pkg/gofr/external_db.go similarity index 100% rename from pkg/gofr/externalDB.go rename to pkg/gofr/external_db.go diff --git a/pkg/gofr/file/zip.go b/pkg/gofr/file/zip.go index 9598794bb..8dfc4027e 100644 --- a/pkg/gofr/file/zip.go +++ b/pkg/gofr/file/zip.go @@ -95,7 +95,7 @@ func copyToBuffer(f io.ReadCloser, size uint64) (*bytes.Buffer, error) { } buf := new(bytes.Buffer) - if n, err := io.CopyN(buf, f, maxFileSize); err != nil && err != io.EOF && uint64(n) < size { + if n, err := io.CopyN(buf, f, maxFileSize); err != nil && !errors.Is(err, io.EOF) && uint64(n) < size { f.Close() return nil, err diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 90066cf6b..9c3aa9a74 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -85,7 +85,11 @@ func New() *App { app.httpServer = newHTTPServer(app.container, port, middleware.GetConfigs(app.Config)) - // GRPC Server + if app.Config.Get("APP_ENV") == "DEBUG" { + app.httpServer.RegisterProfilingRoutes() + } + + // gRPC Server port, err = strconv.Atoi(app.Config.Get("GRPC_PORT")) if err != nil || port <= 0 { port = defaultGRPCPort @@ -280,7 +284,9 @@ func (a *App) SubCommand(pattern string, handler Handler, options ...Options) { func (a *App) Migrate(migrationsMap map[int64]migration.Migrate) { // TODO : Move panic recovery at central location which will manage for all the different cases. - defer panicRecovery(a.container.Logger) + defer func() { + panicRecovery(recover(), a.container.Logger) + }() migration.Run(migrationsMap, a.container) } diff --git a/pkg/gofr/grpc.go b/pkg/gofr/grpc.go index 939fea12b..be65ef760 100644 --- a/pkg/gofr/grpc.go +++ b/pkg/gofr/grpc.go @@ -6,10 +6,10 @@ import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" - grpc2 "gofr.dev/pkg/gofr/grpc" "google.golang.org/grpc" "gofr.dev/pkg/gofr/container" + grpc2 "gofr.dev/pkg/gofr/grpc" ) type grpcServer struct { diff --git a/pkg/gofr/grpc/log.go b/pkg/gofr/grpc/log.go index 0706952f9..dbcdbf808 100644 --- a/pkg/gofr/grpc/log.go +++ b/pkg/gofr/grpc/log.go @@ -28,7 +28,7 @@ type RPCLog struct { } func (l RPCLog) PrettyPrint(writer io.Writer) { - /// checking the length of status code to match the spacing that is being done in HTTP logs after status codes + // checking the length of status code to match the spacing that is being done in HTTP logs after status codes statusCodeLen := 9 - int(math.Log10(float64(l.StatusCode))) + 1 fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d"+ diff --git a/pkg/gofr/grpc/log_test.go b/pkg/gofr/grpc/log_test.go index af2f8594d..1bc1b42c5 100644 --- a/pkg/gofr/grpc/log_test.go +++ b/pkg/gofr/grpc/log_test.go @@ -36,7 +36,7 @@ func TestRPCLog_String(t *testing.T) { func TestLoggingInterceptor(t *testing.T) { var ( - err = errors.New("DB error") //nolint:goerr113 // We are testing if a dynamic error would work + err = errors.New("DB error") //nolint:err113 // We are testing if a dynamic error would work key contextKey = "id" successHandler = func(context.Context, interface{}) (interface{}, error) { diff --git a/pkg/gofr/handler.go b/pkg/gofr/handler.go index 65b295f39..b7b605326 100644 --- a/pkg/gofr/handler.go +++ b/pkg/gofr/handler.go @@ -43,18 +43,13 @@ type handler struct { func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := newContext(gofrHTTP.NewResponder(w, r.Method), gofrHTTP.NewRequest(r), h.container) - var ( - ctx context.Context - cancel context.CancelFunc - ) - if websocket.IsWebSocketUpgrade(r) { // If the request is a WebSocket upgrade, do not apply the timeout - ctx = r.Context() + c.Context = r.Context() } else if h.requestTimeout != "" { reqTimeout := h.setContextTimeout(h.requestTimeout) - ctx, cancel = context.WithTimeout(r.Context(), time.Duration(reqTimeout)*time.Second) + ctx, cancel := context.WithTimeout(r.Context(), time.Duration(reqTimeout)*time.Second) defer cancel() c.Context = ctx @@ -69,7 +64,9 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ) go func() { - defer panicRecoveryHandler(h.container, panicked) + defer func() { + panicRecoveryHandler(recover(), h.container, panicked) + }() // Execute the handler function result, err = h.function(c) @@ -79,7 +76,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { select { case <-c.Context.Done(): // If the context's deadline has been exceeded, return a timeout error response - if errors.Is(ctx.Err(), context.DeadlineExceeded) { + if errors.Is(c.Err(), context.DeadlineExceeded) { err = gofrHTTP.ErrorRequestTimeout{} } case <-done: @@ -131,13 +128,14 @@ func (h handler) setContextTimeout(timeout string) int { return reqTimeout } -func panicRecoveryHandler(log logging.Logger, panicked chan struct{}) { - re := recover() - if re != nil { - close(panicked) - log.Error(panicLog{ - Error: fmt.Sprint(re), - StackTrace: string(debug.Stack()), - }) +func panicRecoveryHandler(re any, log logging.Logger, panicked chan struct{}) { + if re == nil { + return } + + close(panicked) + log.Error(panicLog{ + Error: fmt.Sprint(re), + StackTrace: string(debug.Stack()), + }) } diff --git a/pkg/gofr/http/middleware/apikey_auth_test.go b/pkg/gofr/http/middleware/apikey_auth_test.go index 57a148c10..5ea6f8008 100644 --- a/pkg/gofr/http/middleware/apikey_auth_test.go +++ b/pkg/gofr/http/middleware/apikey_auth_test.go @@ -28,7 +28,7 @@ func Test_ApiKeyAuthMiddleware(t *testing.T) { return apiKey == validKey2 } - req, err := http.NewRequestWithContext(context.Background(), "GET", "/", http.NoBody) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody) if err != nil { t.Fatal(err) } diff --git a/pkg/gofr/http/middleware/basic_auth_test.go b/pkg/gofr/http/middleware/basic_auth_test.go index c471f5d84..a3a10bfb7 100644 --- a/pkg/gofr/http/middleware/basic_auth_test.go +++ b/pkg/gofr/http/middleware/basic_auth_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gofr.dev/pkg/gofr/container" ) diff --git a/pkg/gofr/http/middleware/logger.go b/pkg/gofr/http/middleware/logger.go index fbfa0b968..61e671169 100644 --- a/pkg/gofr/http/middleware/logger.go +++ b/pkg/gofr/http/middleware/logger.go @@ -98,7 +98,9 @@ func Logging(logger logger) func(inner http.Handler) http.Handler { } }(srw, r) - defer panicRecovery(srw, logger) + defer func() { + panicRecovery(recover(), srw, logger) + }() inner.ServeHTTP(srw, r) }) @@ -124,27 +126,27 @@ type panicLog struct { StackTrace string `json:"stack_trace,omitempty"` } -func panicRecovery(w http.ResponseWriter, logger logger) { - re := recover() - - if re != nil { - var e string - switch t := re.(type) { - case string: - e = t - case error: - e = t.Error() - default: - e = "Unknown panic type" - } - logger.Error(panicLog{ - Error: e, - StackTrace: string(debug.Stack()), - }) - - w.WriteHeader(http.StatusInternalServerError) +func panicRecovery(re any, w http.ResponseWriter, logger logger) { + if re == nil { + return + } - res := map[string]interface{}{"code": http.StatusInternalServerError, "status": "ERROR", "message": "Some unexpected error has occurred"} - _ = json.NewEncoder(w).Encode(res) + var e string + switch t := re.(type) { + case string: + e = t + case error: + e = t.Error() + default: + e = "Unknown panic type" } + logger.Error(panicLog{ + Error: e, + StackTrace: string(debug.Stack()), + }) + + w.WriteHeader(http.StatusInternalServerError) + + res := map[string]interface{}{"code": http.StatusInternalServerError, "status": "ERROR", "message": "Some unexpected error has occurred"} + _ = json.NewEncoder(w).Encode(res) } diff --git a/pkg/gofr/http/middleware/logger_test.go b/pkg/gofr/http/middleware/logger_test.go index cdd974b8f..8e8db57c3 100644 --- a/pkg/gofr/http/middleware/logger_test.go +++ b/pkg/gofr/http/middleware/logger_test.go @@ -17,7 +17,7 @@ func Test_getIPAddress(t *testing.T) { { // When RemoteAddr is set addr := "0.0.0.0:8080" - req, err := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) assert.Nil(t, err, "TEST Failed.\n") @@ -30,7 +30,7 @@ func Test_getIPAddress(t *testing.T) { { // When `X-Forwarded-For` header is set addr := "192.168.0.1:8080" - req, err := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) assert.Nil(t, err, "TEST Failed.\n") @@ -43,7 +43,7 @@ func Test_getIPAddress(t *testing.T) { func Test_LoggingMiddleware(t *testing.T) { logs := testutil.StdoutOutputForFunc(func() { - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) rr := httptest.NewRecorder() @@ -57,7 +57,7 @@ func Test_LoggingMiddleware(t *testing.T) { func Test_LoggingMiddlewareError(t *testing.T) { logs := testutil.StderrOutputForFunc(func() { - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) rr := httptest.NewRecorder() @@ -83,7 +83,7 @@ func testHandlerError(w http.ResponseWriter, _ *http.Request) { func Test_LoggingMiddlewareStringPanicHandling(t *testing.T) { logs := testutil.StderrOutputForFunc(func() { - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) rr := httptest.NewRecorder() @@ -102,7 +102,7 @@ func testStringPanicHandler(_ http.ResponseWriter, r *http.Request) { func Test_LoggingMiddlewareErrorPanicHandling(t *testing.T) { logs := testutil.StderrOutputForFunc(func() { - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) rr := httptest.NewRecorder() @@ -121,7 +121,7 @@ func testErrorPanicHandler(http.ResponseWriter, *http.Request) { func Test_LoggingMiddlewareUnknownPanicHandling(t *testing.T) { logs := testutil.StderrOutputForFunc(func() { - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dummy", http.NoBody) rr := httptest.NewRecorder() diff --git a/pkg/gofr/http/middleware/metrics_test.go b/pkg/gofr/http/middleware/metrics_test.go index 305705131..abba8e576 100644 --- a/pkg/gofr/http/middleware/metrics_test.go +++ b/pkg/gofr/http/middleware/metrics_test.go @@ -2,13 +2,12 @@ package middleware import ( "context" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/mock" - "net/http" "net/http/httptest" "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/mock" ) type mockMetrics struct { diff --git a/pkg/gofr/http/middleware/tracer_test.go b/pkg/gofr/http/middleware/tracer_test.go index f57efb4f4..1347dc72c 100644 --- a/pkg/gofr/http/middleware/tracer_test.go +++ b/pkg/gofr/http/middleware/tracer_test.go @@ -23,7 +23,7 @@ func TestTrace(_ *testing.T) { otel.SetTracerProvider(tp) handler := Tracer(&MockHandlerForTracing{}) - req := httptest.NewRequest("GET", "/dummy", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/dummy", http.NoBody) recorder := httptest.NewRecorder() diff --git a/pkg/gofr/http/middleware/web_socket.go b/pkg/gofr/http/middleware/web_socket.go index 34637717f..346e99df1 100644 --- a/pkg/gofr/http/middleware/web_socket.go +++ b/pkg/gofr/http/middleware/web_socket.go @@ -4,10 +4,10 @@ import ( "context" "net/http" + gorillaWebsocket "github.com/gorilla/websocket" + "gofr.dev/pkg/gofr/container" "gofr.dev/pkg/gofr/websocket" - - gorillaWebsocket "github.com/gorilla/websocket" ) // WSHandlerUpgrade middleware upgrades the incoming http request to a websocket connection using websocket upgrader. diff --git a/pkg/gofr/http/middleware/web_socket_test.go b/pkg/gofr/http/middleware/web_socket_test.go index f20de6b89..0e65a7909 100644 --- a/pkg/gofr/http/middleware/web_socket_test.go +++ b/pkg/gofr/http/middleware/web_socket_test.go @@ -9,6 +9,7 @@ import ( "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr/container" gofrWebSocket "gofr.dev/pkg/gofr/websocket" ) @@ -16,6 +17,8 @@ import ( var errConnection = errors.New("can't create connection") func initializeWebSocketMocks(t *testing.T) (gofrWebSocket.MockUpgrader, *gofrWebSocket.Manager) { + t.Helper() + mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -68,7 +71,7 @@ func Test_WSConnectionCreate_Success(t *testing.T) { w.WriteHeader(http.StatusOK) }) - req := httptest.NewRequest("GET", "/ws", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/ws", http.NoBody) req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") diff --git a/pkg/gofr/http/multipartFileBind.go b/pkg/gofr/http/multipart_file_bind.go similarity index 100% rename from pkg/gofr/http/multipartFileBind.go rename to pkg/gofr/http/multipart_file_bind.go diff --git a/pkg/gofr/http/multipartFileBind_test.go b/pkg/gofr/http/multipart_file_bind_test.go similarity index 100% rename from pkg/gofr/http/multipartFileBind_test.go rename to pkg/gofr/http/multipart_file_bind_test.go diff --git a/pkg/gofr/http/request.go b/pkg/gofr/http/request.go index fa7bdddbf..e3d0b0d32 100644 --- a/pkg/gofr/http/request.go +++ b/pkg/gofr/http/request.go @@ -96,12 +96,12 @@ func (r *Request) body() ([]byte, error) { func (r *Request) bindMultipart(ptr any) error { ptrVal := reflect.ValueOf(ptr) - if ptrVal.Kind() == reflect.Ptr { - ptrVal = ptrVal.Elem() - } else { + if ptrVal.Kind() != reflect.Ptr { return errNonPointerBind } + ptrVal = ptrVal.Elem() + if err := r.req.ParseMultipartForm(defaultMaxMemory); err != nil { return err } diff --git a/pkg/gofr/http/request_test.go b/pkg/gofr/http/request_test.go index 58c943d92..78cb005b1 100644 --- a/pkg/gofr/http/request_test.go +++ b/pkg/gofr/http/request_test.go @@ -17,14 +17,14 @@ import ( ) func TestParam(t *testing.T) { - req := NewRequest(httptest.NewRequest("GET", "/abc?a=b", http.NoBody)) + req := NewRequest(httptest.NewRequest(http.MethodGet, "/abc?a=b", http.NoBody)) if req.Param("a") != "b" { t.Error("Can not parse the request params") } } func TestBind(t *testing.T) { - r := httptest.NewRequest("POST", "/abc", strings.NewReader(`{"a": "b", "b": 5}`)) + r := httptest.NewRequest(http.MethodPost, "/abc", strings.NewReader(`{"a": "b", "b": 5}`)) r.Header.Set("content-type", "application/json") req := NewRequest(r) @@ -125,7 +125,7 @@ func TestBind_FileSuccess(t *testing.T) { } func TestBind_NoContentType(t *testing.T) { - req := NewRequest(httptest.NewRequest("POST", "/abc", strings.NewReader(`{"a": "b", "b": 5}`))) + req := NewRequest(httptest.NewRequest(http.MethodPost, "/abc", strings.NewReader(`{"a": "b", "b": 5}`))) x := struct { A string `json:"a"` B int `json:"b"` @@ -149,6 +149,8 @@ func Test_GetContext(t *testing.T) { } func generateMultipartRequestZip(t *testing.T) *http.Request { + t.Helper() + var buf bytes.Buffer writer := multipart.NewWriter(&buf) @@ -195,7 +197,7 @@ func generateMultipartRequestZip(t *testing.T) *http.Request { writer.Close() // Create a new HTTP request with the multipart data - req := httptest.NewRequest("POST", "/upload", &buf) + req := httptest.NewRequest(http.MethodPost, "/upload", &buf) req.Header.Set("content-type", writer.FormDataContentType()) return req diff --git a/pkg/gofr/http/router_test.go b/pkg/gofr/http/router_test.go index afc6dba7f..6647a6d99 100644 --- a/pkg/gofr/http/router_test.go +++ b/pkg/gofr/http/router_test.go @@ -24,12 +24,12 @@ func TestRouter(t *testing.T) { router := NewRouter() // Add a test handler to the router - router.Add("GET", "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + router.Add(http.MethodGet, "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) // Send a request to the test handler - req := httptest.NewRequest("GET", "/test", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -54,12 +54,12 @@ func TestRouterWithMiddleware(t *testing.T) { }) // Add a test handler to the router - router.Add("GET", "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + router.Add(http.MethodGet, "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) // Send a request to the test handler - req := httptest.NewRequest("GET", "/test", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -87,7 +87,7 @@ func TestRouter_AddStaticFiles(t *testing.T) { router.AddStaticFiles("/gofr", currentWorkingDir+"/testDir") // Send a request to the test handler - req := httptest.NewRequest("GET", "/gofr/indexTest.html", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/gofr/indexTest.html", http.NoBody) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -95,7 +95,7 @@ func TestRouter_AddStaticFiles(t *testing.T) { assert.Equal(t, http.StatusOK, rec.Code) // Send a request to the test handler - req = httptest.NewRequest("GET", "/gofr/openapi.json", http.NoBody) + req = httptest.NewRequest(http.MethodGet, "/gofr/openapi.json", http.NoBody) rec = httptest.NewRecorder() router.ServeHTTP(rec, req) diff --git a/pkg/gofr/httpServer.go b/pkg/gofr/http_server.go similarity index 71% rename from pkg/gofr/httpServer.go rename to pkg/gofr/http_server.go index 99742bdc3..90123cd36 100644 --- a/pkg/gofr/httpServer.go +++ b/pkg/gofr/http_server.go @@ -3,6 +3,7 @@ package gofr import ( "fmt" "net/http" + "net/http/pprof" "time" "gofr.dev/pkg/gofr/container" @@ -36,6 +37,15 @@ func newHTTPServer(c *container.Container, port int, middlewareConfigs map[strin } } +func (s *httpServer) RegisterProfilingRoutes() { + s.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + s.router.HandleFunc("/debug/pprof/profile", pprof.Profile) + s.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + s.router.HandleFunc("/debug/pprof/trace", pprof.Trace) + + s.router.NewRoute().Methods(http.MethodGet).PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) +} + func (s *httpServer) Run(c *container.Container) { var srv *http.Server diff --git a/pkg/gofr/httpServer_test.go b/pkg/gofr/http_server_test.go similarity index 60% rename from pkg/gofr/httpServer_test.go rename to pkg/gofr/http_server_test.go index c3b7819a7..bbb80ad32 100644 --- a/pkg/gofr/httpServer_test.go +++ b/pkg/gofr/http_server_test.go @@ -3,6 +3,8 @@ package gofr import ( "context" "net/http" + "net/http/httptest" + "strconv" "testing" "time" @@ -51,3 +53,35 @@ func TestRun_ServerStartsListening(t *testing.T) { resp.Body.Close() } + +func TestRegisterProfillingRoutes(t *testing.T) { + c := &container.Container{ + Logger: logging.NewLogger(logging.INFO), + } + + server := &httpServer{ + router: gofrHTTP.NewRouter(), + port: 8080, + } + + server.RegisterProfilingRoutes() + + server.Run(c) + + // Test if the expected handlers are registered for the pprof endpoints + expectedRoutes := []string{ + "/debug/pprof/", + "/debug/pprof/cmdline", + "/debug/pprof/symbol", + } + + serverURL := "http://localhost:" + strconv.Itoa(8000) + + for _, route := range expectedRoutes { + r := httptest.NewRequest(http.MethodGet, serverURL+route, http.NoBody) + rr := httptest.NewRecorder() + server.router.ServeHTTP(rr, r) + + assert.Equal(t, http.StatusOK, rr.Code) + } +} diff --git a/pkg/gofr/logging/level.go b/pkg/gofr/logging/level.go index 38a76ce50..1344a035f 100644 --- a/pkg/gofr/logging/level.go +++ b/pkg/gofr/logging/level.go @@ -48,7 +48,7 @@ func (l Level) String() string { } } -//nolint:gomnd // Color codes are sent as numbers +//nolint:mnd // Color codes are sent as numbers func (l Level) color() uint { switch l { case ERROR, FATAL: diff --git a/pkg/gofr/logging/logger_test.go b/pkg/gofr/logging/logger_test.go index 3879f4594..f009d1e09 100644 --- a/pkg/gofr/logging/logger_test.go +++ b/pkg/gofr/logging/logger_test.go @@ -130,6 +130,8 @@ func TestLogger_LevelFatal(t *testing.T) { } func assertMessageInJSONLog(t *testing.T, logLine, expectation string) { + t.Helper() + var l logEntry _ = json.Unmarshal([]byte(logLine), &l) diff --git a/pkg/gofr/logging/remotelogger/dynamicLevelLogger.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go similarity index 100% rename from pkg/gofr/logging/remotelogger/dynamicLevelLogger.go rename to pkg/gofr/logging/remotelogger/dynamic_level_logger.go diff --git a/pkg/gofr/logging/remotelogger/dynamicLevelLogger_test.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go similarity index 99% rename from pkg/gofr/logging/remotelogger/dynamicLevelLogger_test.go rename to pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go index 71bda6f4d..c273d0edb 100644 --- a/pkg/gofr/logging/remotelogger/dynamicLevelLogger_test.go +++ b/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "gofr.dev/pkg/gofr/logging" "gofr.dev/pkg/gofr/service" "gofr.dev/pkg/gofr/testutil" diff --git a/pkg/gofr/metrics/exporters/exporter.go b/pkg/gofr/metrics/exporters/exporter.go index 3efc81fdf..8d2273a99 100644 --- a/pkg/gofr/metrics/exporters/exporter.go +++ b/pkg/gofr/metrics/exporters/exporter.go @@ -31,30 +31,30 @@ func Prometheus(appName, appVersion string) metric.Meter { // TODO : OTLPStdOut and OTLPMetricHTTP are not being used but has to be modified such that user can decide the exporter. // func OTLPStdOut(appName, appVersion string) metric.Meter { -// exporter, err := stdoutmetric.New() -// if err != nil { -// return nil -// } +// exporter, err := stdoutmetric.New() +// if err != nil { +// return nil +// } // -// meter := metricSdk.NewMeterProvider( -// metricSdk.WithResource(resource.NewSchemaless(semconv.ServiceName(appName))), -// metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter, -// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion)) +// meter := metricSdk.NewMeterProvider( +// metricSdk.WithResource(resource.NewSchemaless(semconv.ServiceName(appName))), +// metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter, +// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion)) // -// return meter -//} +// return meter +// } // // func OTLPMetricHTTP(appName, appVersion string) metric.Meter { -// exporter, err := otlpmetrichttp.New(nil, -// otlpmetrichttp.WithInsecure(), -// otlpmetrichttp.WithURLPath("/metrics"), -// otlpmetrichttp.WithEndpoint("localhost:8000")) -// if err != nil { -// return nil -// } +// exporter, err := otlpmetrichttp.New(nil, +// otlpmetrichttp.WithInsecure(), +// otlpmetrichttp.WithURLPath("/metrics"), +// otlpmetrichttp.WithEndpoint("localhost:8000")) +// if err != nil { +// return nil +// } // -// meter := metricSdk.NewMeterProvider(metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter, -// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion)) +// meter := metricSdk.NewMeterProvider(metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter, +// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion)) // -// return meter -//} +// return meter +// } diff --git a/pkg/gofr/metricsServer.go b/pkg/gofr/metrics_server.go similarity index 100% rename from pkg/gofr/metricsServer.go rename to pkg/gofr/metrics_server.go diff --git a/pkg/gofr/migration/clickhouse_test.go b/pkg/gofr/migration/clickhouse_test.go index be6301f3b..e26946422 100644 --- a/pkg/gofr/migration/clickhouse_test.go +++ b/pkg/gofr/migration/clickhouse_test.go @@ -13,6 +13,8 @@ import ( ) func clickHouseSetup(t *testing.T) (migrator, *MockClickhouse, *container.Container) { + t.Helper() + ctrl := gomock.NewController(t) mockContainer, _ := container.NewMockContainer(t) diff --git a/pkg/gofr/migration/interface.go b/pkg/gofr/migration/interface.go index ce7934c31..7fa22109e 100644 --- a/pkg/gofr/migration/interface.go +++ b/pkg/gofr/migration/interface.go @@ -6,6 +6,7 @@ import ( "time" goRedis "github.com/redis/go-redis/v9" + "gofr.dev/pkg/gofr/container" ) @@ -33,6 +34,8 @@ type Clickhouse interface { Exec(ctx context.Context, query string, args ...any) error Select(ctx context.Context, dest any, query string, args ...any) error AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error + + HealthCheck(ctx context.Context) (any, error) } // keeping the migrator interface unexported as, right now it is not being implemented directly, by the externalDB drivers. diff --git a/pkg/gofr/migration/migration_test.go b/pkg/gofr/migration/migration_test.go index ed4f4095f..565da3470 100644 --- a/pkg/gofr/migration/migration_test.go +++ b/pkg/gofr/migration/migration_test.go @@ -56,6 +56,8 @@ func Test_getMigratorDBInitialisation(t *testing.T) { } func initialiseClickHouseRunMocks(t *testing.T) (*MockClickhouse, *container.Container) { + t.Helper() + mockClickHouse := NewMockClickhouse(gomock.NewController(t)) mockContainer, _ := container.NewMockContainer(t) diff --git a/pkg/gofr/migration/mock_interface.go b/pkg/gofr/migration/mock_interface.go index 9a7db0462..7993a4855 100644 --- a/pkg/gofr/migration/mock_interface.go +++ b/pkg/gofr/migration/mock_interface.go @@ -17,6 +17,7 @@ import ( redis "github.com/redis/go-redis/v9" gomock "go.uber.org/mock/gomock" + container "gofr.dev/pkg/gofr/container" ) @@ -337,6 +338,21 @@ func (mr *MockClickhouseMockRecorder) Exec(ctx, query any, args ...any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouse)(nil).Exec), varargs...) } +// HealthCheck mocks base method. +func (m *MockClickhouse) HealthCheck(ctx context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", ctx) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockClickhouseMockRecorder) HealthCheck(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouse)(nil).HealthCheck), ctx) +} + // Select mocks base method. func (m *MockClickhouse) Select(ctx context.Context, dest any, query string, args ...any) error { m.ctrl.T.Helper() diff --git a/pkg/gofr/service/basic_auth_test.go b/pkg/gofr/service/basic_auth_test.go index f7605764c..a02edc7a2 100644 --- a/pkg/gofr/service/basic_auth_test.go +++ b/pkg/gofr/service/basic_auth_test.go @@ -25,7 +25,7 @@ func TestBasicAuthProvider_Get(t *testing.T) { // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAuthHeaders(r, t) + checkAuthHeaders(t, r) assert.Equal(t, http.MethodGet, r.Method) w.WriteHeader(http.StatusOK) @@ -66,7 +66,7 @@ func TestBasicAuthProvider_Post(t *testing.T) { // Create a mock HTTP server (verify POST method) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAuthHeaders(r, t) + checkAuthHeaders(t, r) assert.Equal(t, http.MethodPost, r.Method) w.WriteHeader(http.StatusCreated) @@ -98,7 +98,7 @@ func TestBasicAuthProvider_Put(t *testing.T) { // Create a mock HTTP server (verify PUT method) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAuthHeaders(r, t) + checkAuthHeaders(t, r) assert.Equal(t, http.MethodPut, r.Method) w.WriteHeader(http.StatusOK) @@ -130,7 +130,7 @@ func TestBasicAuthProvider_Patch(t *testing.T) { // Create a mock HTTP server (verify PATCH method) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAuthHeaders(r, t) + checkAuthHeaders(t, r) assert.Equal(t, http.MethodPatch, r.Method) w.WriteHeader(http.StatusOK) @@ -161,7 +161,7 @@ func TestBasicAuthProvider_Delete(t *testing.T) { // Create a mock HTTP server (verify DELETE method) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAuthHeaders(r, t) + checkAuthHeaders(t, r) assert.Equal(t, http.MethodDelete, r.Method) w.WriteHeader(http.StatusNoContent) @@ -183,7 +183,9 @@ func TestBasicAuthProvider_Delete(t *testing.T) { assert.Nil(t, err) } -func checkAuthHeaders(r *http.Request, t *testing.T) { +func checkAuthHeaders(t *testing.T, r *http.Request) { + t.Helper() + authHeader := r.Header.Get("Authorization") if authHeader == "" { diff --git a/pkg/gofr/service/circuit_breaker_test.go b/pkg/gofr/service/circuit_breaker_test.go index 0d6ad8f34..681b72faf 100644 --- a/pkg/gofr/service/circuit_breaker_test.go +++ b/pkg/gofr/service/circuit_breaker_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.opentelemetry.io/otel" + "gofr.dev/pkg/gofr/logging" "gofr.dev/pkg/gofr/testutil" ) diff --git a/pkg/gofr/service/health.go b/pkg/gofr/service/health.go index e463c3677..c1ca2525c 100644 --- a/pkg/gofr/service/health.go +++ b/pkg/gofr/service/health.go @@ -3,11 +3,13 @@ package service import ( "context" "net/http" + "time" ) const ( - serviceUp = "UP" - serviceDown = "DOWN" + serviceUp = "UP" + serviceDown = "DOWN" + defaultTimeout = 5 ) type Health struct { @@ -16,14 +18,20 @@ type Health struct { } func (h *httpService) HealthCheck(ctx context.Context) *Health { - return h.getHealthResponseForEndpoint(ctx, ".well-known/alive") + return h.getHealthResponseForEndpoint(ctx, ".well-known/alive", defaultTimeout) } -func (h *httpService) getHealthResponseForEndpoint(ctx context.Context, endpoint string) *Health { +func (h *httpService) getHealthResponseForEndpoint(ctx context.Context, endpoint string, timeout int) *Health { var healthResponse = Health{ Details: make(map[string]interface{}), } + // create a new context with timeout for healthCheck call. + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Duration(timeout)*time.Second) + defer cancel() + + // send a new context as we can have downstream services taking too long + // which may cancel the original health check http request resp, err := h.Get(ctx, endpoint, nil) if err != nil || resp == nil { diff --git a/pkg/gofr/service/health_config.go b/pkg/gofr/service/health_config.go index 5614d62de..23c920db8 100644 --- a/pkg/gofr/service/health_config.go +++ b/pkg/gofr/service/health_config.go @@ -4,20 +4,28 @@ import "context" type HealthConfig struct { HealthEndpoint string + Timeout int } func (h *HealthConfig) AddOption(svc HTTP) HTTP { + // if timeout is not provided we set a convenient default timeout. + if h.Timeout == 0 { + h.Timeout = defaultTimeout + } + return &customHealthService{ healthEndpoint: h.HealthEndpoint, + timeout: h.Timeout, HTTP: svc, } } type customHealthService struct { healthEndpoint string + timeout int HTTP } func (c *customHealthService) HealthCheck(ctx context.Context) *Health { - return c.HTTP.getHealthResponseForEndpoint(ctx, c.healthEndpoint) + return c.HTTP.getHealthResponseForEndpoint(ctx, c.healthEndpoint, c.timeout) } diff --git a/pkg/gofr/service/health_test.go b/pkg/gofr/service/health_test.go index 0d1059429..4d072c100 100644 --- a/pkg/gofr/service/health_test.go +++ b/pkg/gofr/service/health_test.go @@ -7,24 +7,24 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr/logging" + "gofr.dev/pkg/gofr/testutil" ) func TestHTTPService_HealthCheck(t *testing.T) { service, server, metrics := initializeTest(t, "alive", http.StatusOK) defer server.Close() - ctx := context.Background() - - metrics.EXPECT().RecordHistogram(ctx, "app_http_service_response", gomock.Any(), "path", server.URL, + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL, "method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusOK)).Times(1) // when params value is of type []string then last value is sent in request - resp := service.HealthCheck(ctx) + resp := service.HealthCheck(context.Background()) assert.Equal(t, &Health{Status: serviceUp, Details: map[string]interface{}{"host": server.URL[7:]}}, resp, "TEST[%d], Failed.\n%s") @@ -34,13 +34,11 @@ func TestHTTPService_HealthCheckCustomURL(t *testing.T) { service, server, metrics := initializeTest(t, "ready", http.StatusOK) defer server.Close() - ctx := context.Background() - - metrics.EXPECT().RecordHistogram(ctx, "app_http_service_response", gomock.Any(), "path", server.URL, + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL, "method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusOK)).Times(1) // when params value is of type []string then last value is sent in request - resp := service.HealthCheck(ctx) + resp := service.HealthCheck(context.Background()) assert.Equal(t, &Health{Status: serviceUp, Details: map[string]interface{}{"host": server.URL[7:]}}, resp, "TEST[%d], Failed.\n%s") @@ -49,15 +47,14 @@ func TestHTTPService_HealthCheckCustomURL(t *testing.T) { func TestHTTPService_HealthCheckErrorResponse(t *testing.T) { ctrl := gomock.NewController(t) metrics := NewMockMetrics(ctrl) - ctx := context.Background() - metrics.EXPECT().RecordHistogram(ctx, "app_http_service_response", gomock.Any(), "path", gomock.Any(), + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", gomock.Any(), "method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusInternalServerError)) service := NewHTTPService("http://test", logging.NewMockLogger(logging.INFO), metrics) // when params value is of type []string then last value is sent in request - resp := service.HealthCheck(ctx) + resp := service.HealthCheck(context.Background()) body, _ := json.Marshal(&resp) @@ -68,20 +65,46 @@ func TestHTTPService_HealthCheckDifferentStatusCode(t *testing.T) { service, server, metrics := initializeTest(t, "bad-request", http.StatusBadRequest) defer server.Close() - ctx := context.Background() - - metrics.EXPECT().RecordHistogram(ctx, "app_http_service_response", gomock.Any(), "path", server.URL, + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL, "method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusBadRequest)).AnyTimes() // when params value is of type []string then last value is sent in request - resp := service.HealthCheck(ctx) + resp := service.HealthCheck(context.Background()) assert.Equal(t, &Health{Status: serviceDown, Details: map[string]interface{}{"host": server.URL[7:], "error": "service down"}}, resp, "TEST[%d], Failed.\n%s") } +func TestHTTPService_HealthCheckTimeout(t *testing.T) { + ctrl := gomock.NewController(t) + metrics := NewMockMetrics(ctrl) + server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/.well-known/alive", r.URL.Path) + + time.Sleep(2 * time.Second) + })) + + metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL, + "method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusInternalServerError)).AnyTimes() + + log := testutil.StdoutOutputForFunc(func() { + service := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), metrics, + &HealthConfig{HealthEndpoint: ".well-known/alive", Timeout: 1}) + + resp := service.HealthCheck(context.Background()) + + assert.Equal(t, &Health{Status: serviceDown, + Details: map[string]interface{}{"error": "Get \"" + server.URL + "/.well-known/alive\": context deadline exceeded"}}, + resp, "TEST[%d], Failed.\n%s") + }) + + assert.Contains(t, log, "context deadline exceeded") +} + func initializeTest(t *testing.T, urlSuffix string, statusCode int) (HTTP, *httptest.Server, *MockMetrics) { + t.Helper() + ctrl := gomock.NewController(t) metrics := NewMockMetrics(ctrl) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/gofr/service/new.go b/pkg/gofr/service/new.go index a9ada5bb5..9e2850dba 100644 --- a/pkg/gofr/service/new.go +++ b/pkg/gofr/service/new.go @@ -29,7 +29,7 @@ type HTTP interface { // HealthCheck to get the service health and report it to the current application HealthCheck(ctx context.Context) *Health - getHealthResponseForEndpoint(ctx context.Context, endpoint string) *Health + getHealthResponseForEndpoint(ctx context.Context, endpoint string, timeout int) *Health } type httpClient interface { diff --git a/pkg/gofr/service/oauth_test.go b/pkg/gofr/service/oauth_test.go index bb7b18d04..21ac36ce1 100644 --- a/pkg/gofr/service/oauth_test.go +++ b/pkg/gofr/service/oauth_test.go @@ -15,6 +15,8 @@ import ( ) func oAuthHTTPServer(t *testing.T) *httptest.Server { + t.Helper() + // Start a test HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("Authorization") diff --git a/pkg/gofr/service/retry.go b/pkg/gofr/service/retry.go new file mode 100644 index 000000000..b143aa144 --- /dev/null +++ b/pkg/gofr/service/retry.go @@ -0,0 +1,109 @@ +package service + +import ( + "context" + "net/http" +) + +type RetryConfig struct { + MaxRetries int +} + +func (r *RetryConfig) AddOption(h HTTP) HTTP { + return &retryProvider{ + maxRetries: r.MaxRetries, + HTTP: h, + } +} + +type retryProvider struct { + maxRetries int + + HTTP +} + +func (rp *retryProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, + error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.Get(ctx, path, queryParams) + }) +} + +func (rp *retryProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, + headers map[string]string) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.GetWithHeaders(ctx, path, queryParams, headers) + }) +} + +func (rp *retryProvider) Post(ctx context.Context, path string, queryParams map[string]interface{}, + body []byte) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.Post(ctx, path, queryParams, body) + }) +} + +func (rp *retryProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, + body []byte, + headers map[string]string) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers) + }) +} + +func (rp *retryProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) ( + *http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.Put(ctx, api, queryParams, body) + }) +} + +func (rp *retryProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, + headers map[string]string) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers) + }) +} + +func (rp *retryProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) ( + *http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.Patch(ctx, path, queryParams, body) + }) +} + +func (rp *retryProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, + headers map[string]string) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers) + }) +} + +func (rp *retryProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.Delete(ctx, path, body) + }) +} + +func (rp *retryProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( + *http.Response, error) { + return rp.doWithRetry(func() (*http.Response, error) { + return rp.HTTP.DeleteWithHeaders(ctx, path, body, headers) + }) +} + +func (rp *retryProvider) doWithRetry(reqFunc func() (*http.Response, error)) (*http.Response, error) { + var ( + resp *http.Response + err error + ) + + for i := 0; i < rp.maxRetries; i++ { + resp, err = reqFunc() + if err == nil && resp.StatusCode != http.StatusInternalServerError { + return resp, nil + } + } + + return resp, err +} diff --git a/pkg/gofr/service/retry_test.go b/pkg/gofr/service/retry_test.go new file mode 100644 index 000000000..9219f0eab --- /dev/null +++ b/pkg/gofr/service/retry_test.go @@ -0,0 +1,224 @@ +package service + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "gofr.dev/pkg/gofr/logging" +) + +type mockHTTP struct{} + +func (m *mockHTTP) HealthCheck(_ context.Context) *Health { + return &Health{ + Status: "UP", + Details: map[string]interface{}{"host": "http://test.com"}, + } +} + +func (m *mockHTTP) getHealthResponseForEndpoint(_ context.Context, _ string, _ int) *Health { + return &Health{ + Status: "UP", + Details: map[string]interface{}{"host": "http://test.com"}, + } +} + +func (m *mockHTTP) Get(_ context.Context, _ string, _ map[string]interface{}) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) GetWithHeaders(_ context.Context, _ string, _ map[string]interface{}, _ map[string]string) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) Post(_ context.Context, _ string, _ map[string]interface{}, _ []byte) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusCreated, Body: http.NoBody}, nil +} + +func (m *mockHTTP) PostWithHeaders(_ context.Context, _ string, _ map[string]interface{}, _ []byte, + _ map[string]string) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusCreated, Body: http.NoBody}, nil +} + +func (m *mockHTTP) Put(_ context.Context, _ string, _ map[string]interface{}, _ []byte) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) PutWithHeaders(_ context.Context, _ string, _ map[string]interface{}, _ []byte, + _ map[string]string) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) Patch(_ context.Context, _ string, _ map[string]interface{}, _ []byte) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) PatchWithHeaders(_ context.Context, _ string, _ map[string]interface{}, _ []byte, + _ map[string]string) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func (m *mockHTTP) Delete(_ context.Context, _ string, _ []byte) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody}, nil +} + +func (m *mockHTTP) DeleteWithHeaders(_ context.Context, _ string, _ []byte, _ map[string]string) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody}, nil +} + +func TestRetryProvider_Get(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the GET request + resp, err := retryHTTP.Get(context.Background(), "/test", nil) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRetryProvider_GetWithHeaders(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the GET request with headers + resp, err := retryHTTP.GetWithHeaders(context.Background(), "/test", nil, + map[string]string{"Content-Type": "application/json"}) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRetryProvider_Post(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the POST request + resp, err := retryHTTP.Post(context.Background(), "/test", nil, []byte("body")) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) +} + +func TestRetryProvider_PostWithHeaders(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the POST request with headers + resp, err := retryHTTP.PostWithHeaders(context.Background(), "/test", nil, []byte("body"), + map[string]string{"Content-Type": "application/json"}) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) +} + +func TestRetryProvider_Put(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the PUT request + resp, err := retryHTTP.Put(context.Background(), "/test", nil, []byte("body")) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRetryProvider_PutWithHeaders(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the PUT request with headers + resp, err := retryHTTP.PutWithHeaders(context.Background(), "/test", nil, []byte("body"), + map[string]string{"Content-Type": "application/json"}) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRetryProvider_Patch_WithError(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + checkAuthHeaders(t, r) + assert.Equal(t, http.MethodPatch, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + // Create a new HTTP service instance with basic auth + httpService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &RetryConfig{MaxRetries: 5}) + + // Make the PATCH request + resp, err := httpService.Patch(context.Background(), "/test", nil, []byte("body")) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) +} + +func TestRetryProvider_PatchWithHeaders(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the PATCH request with headers + resp, err := retryHTTP.PatchWithHeaders(context.Background(), "/test", nil, []byte("body"), + map[string]string{"Content-Type": "application/json"}) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRetryProvider_Delete(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the DELETE request + resp, err := retryHTTP.Delete(context.Background(), "/test", nil) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNoContent, resp.StatusCode) +} +func TestRetryProvider_DeleteWithHeaders(t *testing.T) { + mockHTTP := &mockHTTP{} + retryConfig := &RetryConfig{MaxRetries: 3} + retryHTTP := retryConfig.AddOption(mockHTTP) + + // Make the DELETE request with headers + resp, err := retryHTTP.DeleteWithHeaders(context.Background(), "/test", []byte("body"), + map[string]string{"Content-Type": "application/json"}) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNoContent, resp.StatusCode) +} diff --git a/pkg/gofr/subscriber.go b/pkg/gofr/subscriber.go index b230c1973..08e160205 100644 --- a/pkg/gofr/subscriber.go +++ b/pkg/gofr/subscriber.go @@ -43,7 +43,10 @@ func (s *SubscriptionManager) startSubscriber(topic string, handler SubscribeFun ctx := newContext(nil, msg, s.container) err = func(ctx *Context) error { // TODO : Move panic recovery at central location which will manage for all the different cases. - defer panicRecovery(ctx.Logger) + defer func() { + panicRecovery(recover(), ctx.Logger) + }() + return handler(ctx) }(ctx) @@ -61,22 +64,22 @@ type panicLog struct { StackTrace string `json:"stack_trace,omitempty"` } -func panicRecovery(log logging.Logger) { - re := recover() +func panicRecovery(re any, log logging.Logger) { + if re == nil { + return + } - if re != nil { - var e string - switch t := re.(type) { - case string: - e = t - case error: - e = t.Error() - default: - e = "Unknown panic type" - } - log.Error(panicLog{ - Error: e, - StackTrace: string(debug.Stack()), - }) + var e string + switch t := re.(type) { + case string: + e = t + case error: + e = t.Error() + default: + e = "Unknown panic type" } + log.Error(panicLog{ + Error: e, + StackTrace: string(debug.Stack()), + }) } diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index 26b076074..2a78e0fb6 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.12.0" +const Framework = "v1.13.0" diff --git a/pkg/gofr/websocket/websocket_test.go b/pkg/gofr/websocket/websocket_test.go index 1dc73cb85..88437f01a 100644 --- a/pkg/gofr/websocket/websocket_test.go +++ b/pkg/gofr/websocket/websocket_test.go @@ -7,10 +7,9 @@ import ( "testing" "time" - "go.uber.org/mock/gomock" - "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" ) func TestConnection_Bind_Success(t *testing.T) {