From 168e8b90cb48a9146b92744e2c3e487103d39880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=C5=A1an=20Borov=C4=8Danin?= Date: Thu, 30 Jul 2026 17:55:57 +0200 Subject: [PATCH] NOISSUE - Move rules engine, alarms, reports, journal and notifications to EE (#3552) Signed-off-by: dusan --- .github/workflows/api-tests.yaml | 78 - .github/workflows/check-generated-files.yaml | 1 - .github/workflows/tests.yaml | 59 +- Makefile | 7 +- alarms/README.md | 193 -- alarms/alarms.go | 123 -- alarms/alarms_test.go | 173 -- alarms/api/doc.go | 6 - alarms/api/endpoint.go | 104 - alarms/api/requests.go | 59 - alarms/api/responses.go | 70 - alarms/api/transport.go | 209 -- alarms/brokers/brokers_fluxmq.go | 53 - alarms/brokers/brokers_nats.go | 53 - alarms/consumer/consumer.go | 57 - alarms/doc.go | 6 - alarms/middleware/authorization.go | 322 --- alarms/middleware/authorization_test.go | 224 -- alarms/middleware/doc.go | 6 - alarms/middleware/logging.go | 155 -- alarms/middleware/metrics.go | 74 - alarms/middleware/tracing.go | 84 - alarms/mocks/repository.go | 370 ---- alarms/mocks/service.go | 389 ---- alarms/operations/operations.go | 52 - alarms/postgres/alarms.go | 510 ----- alarms/postgres/alarms_test.go | 481 ----- alarms/postgres/init.go | 55 - alarms/postgres/setup_test.go | 97 - alarms/service.go | 72 - alarms/service_test.go | 254 --- alarms/status.go | 70 - api/http/common_test.go | 2 +- apidocs/openapi/journal.yaml | 426 ---- auth/cache/tokens_test.go | 2 +- auth/service_test.go | 2 +- cli/config.go | 7 - cli/journal.go | 56 - cmd/alarms/main.go | 204 -- cmd/atom-bootstrap/main.go | 2 +- cmd/auth/main.go | 2 +- cmd/certs/main.go | 2 +- cmd/fluxmq/main.go | 2 +- cmd/journal/main.go | 179 -- cmd/notifications/main.go | 153 -- cmd/postgres-reader/main.go | 2 +- cmd/postgres-writer/main.go | 2 +- cmd/re/main.go | 348 ---- cmd/reports/main.go | 331 --- .../template/reports_default_template.html | 479 ----- cmd/timescale-reader/main.go | 2 +- cmd/timescale-writer/main.go | 2 +- config.toml | 1 - consumers/notifiers/README.md | 4 +- consumers/notifiers/api/endpoint_test.go | 2 +- consumers/notifiers/service_test.go | 2 +- consumers/notifiers/smtp/notifier.go | 2 +- consumers/writers/README.md | 4 +- docker/.env | 27 - docker/README.md | 2 +- docker/addons/bootstrap/docker-compose.yaml | 2 +- .../postgres-writer/docker-compose.yaml | 2 +- docker/addons/prometheus/docker-compose.yaml | 4 +- docker/docker-compose.yaml | 121 +- docker/fluxmq/node1.yaml | 19 +- docker/fluxmq/node2.yaml | 19 +- docker/fluxmq/node3.yaml | 19 +- docker/setup-tls.sh | 11 +- .../templates/invitation-accepted-email.tmpl | 100 - .../templates/invitation-rejected-email.tmpl | 93 - docker/templates/invitation-sent-email.tmpl | 106 - fluxmq/api/grpc/server.go | 2 +- fluxmq/api/grpc/server_test.go | 2 +- fluxmq/api/http/publish.go | 2 +- go.mod | 19 +- go.sum | 319 --- journal/README.md | 195 -- journal/api/doc.go | 6 - journal/api/endpoint.go | 61 - journal/api/endpoint_test.go | 573 ------ journal/api/requests.go | 44 - journal/api/requests_test.go | 156 -- journal/api/responses.go | 48 - journal/api/transport.go | 177 -- journal/doc.go | 7 - journal/events/consumer.go | 100 - journal/events/consumer_test.go | 276 --- journal/events/doc.go | 7 - journal/journal.go | 188 -- journal/journal_test.go | 137 -- journal/middleware/authorization.go | 91 - journal/middleware/doc.go | 9 - journal/middleware/logging.go | 93 - journal/middleware/metrics.go | 57 - journal/middleware/tracing.go | 59 - journal/mocks/doc.go | 5 - journal/mocks/repository.go | 658 ------ journal/mocks/service.go | 245 --- journal/postgres/doc.go | 5 - journal/postgres/errors.go | 24 - journal/postgres/init.go | 93 - journal/postgres/journal.go | 201 -- journal/postgres/journal_test.go | 1332 ------------ journal/postgres/setup_test.go | 93 - journal/postgres/telemetry.go | 254 --- journal/service.go | 392 ---- journal/service_test.go | 147 -- notifications/README.md | 113 - notifications/doc.go | 5 - notifications/emailer/atom_users.go | 70 - notifications/emailer/doc.go | 5 - notifications/emailer/emailer.go | 198 -- notifications/emailer/emailer_test.go | 203 -- notifications/events/consumer.go | 134 -- notifications/events/consumer_test.go | 316 --- notifications/events/doc.go | 5 - notifications/middleware/doc.go | 5 - notifications/middleware/logging.go | 65 - notifications/middleware/logging_test.go | 78 - notifications/middleware/metrics.go | 39 - notifications/middleware/metrics_test.go | 77 - notifications/middleware/tracing.go | 53 - notifications/mocks/doc.go | 5 - notifications/mocks/notifier.go | 100 - notifications/notifier.go | 37 - {internal => pkg}/atom/authz.go | 0 {internal => pkg}/atom/authz_compat.go | 0 {internal => pkg}/atom/authz_test.go | 2 +- {internal => pkg}/atom/bootstrap.go | 0 {internal => pkg}/atom/bootstrap_test.go | 0 {internal => pkg}/atom/client.go | 0 {internal => pkg}/atom/client_test.go | 0 {internal => pkg}/atom/config.go | 0 {internal => pkg}/atom/constants.go | 2 - pkg/atom/doc.go | 7 + {internal => pkg}/atom/grpc_compat.go | 0 {internal => pkg}/atom/grpc_compat_test.go | 0 {internal => pkg}/atom/mapping.go | 0 {internal => pkg}/atom/mapping_test.go | 0 {internal => pkg}/atom/policy.go | 0 {internal => pkg}/atom/policy_service.go | 0 {internal => pkg}/atom/policy_service_test.go | 0 {internal => pkg}/atom/policy_test.go | 2 +- {internal => pkg}/atom/projector.go | 0 {internal => pkg}/atom/service_tokens.go | 2 - {internal => pkg}/atom/service_tokens_test.go | 4 +- {internal => pkg}/atom/test_constants_test.go | 0 {internal => pkg}/atom/token.go | 0 {internal => pkg}/atom/token_test.go | 0 {internal => pkg}/atom/types.go | 0 pkg/authn/atom/authn.go | 2 +- {internal => pkg}/email/README.md | 0 {internal => pkg}/email/doc.go | 0 {internal => pkg}/email/email.go | 0 pkg/emailer/emailer.go | 2 +- .../messaging/writers}/brokers_fluxmq.go | 2 +- .../messaging/writers}/brokers_nats.go | 2 +- pkg/re/events/consumer/decode.go | 207 -- pkg/re/events/consumer/doc.go | 6 - pkg/re/events/consumer/stream.go | 185 -- pkg/readersclient/client.go | 70 + pkg/readersclient/client_test.go | 141 ++ pkg/sdk/README.md | 11 +- pkg/sdk/health.go | 2 - pkg/sdk/journal.go | 63 - pkg/sdk/mocks/sdk.go | 90 - pkg/sdk/sdk.go | 11 - {internal => pkg}/testsutil/common.go | 0 {internal => pkg}/testsutil/redis.go | 0 re/README.md | 320 --- re/api/doc.go | 6 - re/api/endpoints.go | 205 -- re/api/endpoints_test.go | 1284 ------------ re/api/requests.go | 137 -- re/api/responses.go | 138 -- re/api/transport.go | 240 --- re/atom.go | 96 - re/atom_test.go | 41 - re/doc.go | 6 - re/events/doc.go | 6 - re/events/events.go | 189 -- re/events/streams.go | 196 -- re/events/streams_test.go | 576 ------ re/golang.go | 104 - re/handlers.go | 165 -- re/lua.go | 185 -- re/middleware/authorization.go | 210 -- re/middleware/authorization_test.go | 105 - re/middleware/callout.go | 189 -- re/middleware/logging.go | 246 --- re/middleware/metrics.go | 133 -- re/middleware/tracing.go | 133 -- re/mocks/repository.go | 635 ------ re/mocks/service.go | 836 -------- re/operations/operations.go | 62 - re/outputs/alarm.go | 81 - re/outputs/channel.go | 50 - re/outputs/doc.go | 4 - re/outputs/email.go | 54 - re/outputs/outputs.go | 68 - re/outputs/postgres.go | 107 - re/outputs/senml.go | 53 - re/outputs/slack.go | 72 - re/postgres/init.go | 134 -- re/postgres/repository.go | 342 ---- re/postgres/repository_test.go | 998 --------- re/postgres/rule.go | 150 -- re/postgres/setup_test.go | 97 - re/rule.go | 252 --- re/service.go | 196 -- re/service_test.go | 1822 ----------------- re/status.go | 80 - re/status_test.go | 205 -- readers/api/grpc/client.go | 231 +-- readers/postgres/messages_test.go | 2 +- readers/timescale/messages_test.go | 2 +- reports/README.md | 344 ---- reports/api/doc.go | 6 - reports/api/endpoints.go | 294 --- reports/api/endpoints_test.go | 1404 ------------- reports/api/request.go | 242 --- reports/api/response.go | 221 -- reports/api/transport.go | 306 --- reports/atom.go | 91 - reports/events/doc.go | 6 - reports/events/events.go | 68 - reports/events/streams.go | 109 - reports/events/streams_test.go | 632 ------ reports/generator.go | 346 ---- reports/handler.go | 64 - reports/middleware/authorization.go | 230 --- reports/middleware/authorization_test.go | 105 - reports/middleware/callout.go | 216 -- reports/middleware/logging.go | 267 --- reports/middleware/metrics.go | 146 -- reports/middleware/tracing.go | 146 -- reports/mocks/repository.go | 773 ------- reports/mocks/service.go | 944 --------- reports/operations/operations.go | 77 - reports/postgres/errors.go | 27 - reports/postgres/init.go | 100 - reports/postgres/reports.go | 139 -- reports/postgres/repository.go | 451 ---- reports/postgres/repository_test.go | 825 -------- reports/postgres/setup_test.go | 95 - reports/reports.go | 430 ---- reports/service.go | 488 ----- reports/service_test.go | 650 ------ reports/status.go | 80 - reports/template.go | 164 -- reports/template_test.go | 376 ---- reports/tz.go | 25 - scripts/ci.sh | 2 - tools/config/.mockery.yaml | 19 - 254 files changed, 338 insertions(+), 37622 deletions(-) delete mode 100644 alarms/README.md delete mode 100644 alarms/alarms.go delete mode 100644 alarms/alarms_test.go delete mode 100644 alarms/api/doc.go delete mode 100644 alarms/api/endpoint.go delete mode 100644 alarms/api/requests.go delete mode 100644 alarms/api/responses.go delete mode 100644 alarms/api/transport.go delete mode 100644 alarms/brokers/brokers_fluxmq.go delete mode 100644 alarms/brokers/brokers_nats.go delete mode 100644 alarms/consumer/consumer.go delete mode 100644 alarms/doc.go delete mode 100644 alarms/middleware/authorization.go delete mode 100644 alarms/middleware/authorization_test.go delete mode 100644 alarms/middleware/doc.go delete mode 100644 alarms/middleware/logging.go delete mode 100644 alarms/middleware/metrics.go delete mode 100644 alarms/middleware/tracing.go delete mode 100644 alarms/mocks/repository.go delete mode 100644 alarms/mocks/service.go delete mode 100644 alarms/operations/operations.go delete mode 100644 alarms/postgres/alarms.go delete mode 100644 alarms/postgres/alarms_test.go delete mode 100644 alarms/postgres/init.go delete mode 100644 alarms/postgres/setup_test.go delete mode 100644 alarms/service.go delete mode 100644 alarms/service_test.go delete mode 100644 alarms/status.go delete mode 100644 apidocs/openapi/journal.yaml delete mode 100644 cli/journal.go delete mode 100644 cmd/alarms/main.go delete mode 100644 cmd/journal/main.go delete mode 100644 cmd/notifications/main.go delete mode 100644 cmd/re/main.go delete mode 100644 cmd/reports/main.go delete mode 100644 cmd/reports/template/reports_default_template.html delete mode 100644 docker/templates/invitation-accepted-email.tmpl delete mode 100644 docker/templates/invitation-rejected-email.tmpl delete mode 100644 docker/templates/invitation-sent-email.tmpl delete mode 100644 journal/README.md delete mode 100644 journal/api/doc.go delete mode 100644 journal/api/endpoint.go delete mode 100644 journal/api/endpoint_test.go delete mode 100644 journal/api/requests.go delete mode 100644 journal/api/requests_test.go delete mode 100644 journal/api/responses.go delete mode 100644 journal/api/transport.go delete mode 100644 journal/doc.go delete mode 100644 journal/events/consumer.go delete mode 100644 journal/events/consumer_test.go delete mode 100644 journal/events/doc.go delete mode 100644 journal/journal.go delete mode 100644 journal/journal_test.go delete mode 100644 journal/middleware/authorization.go delete mode 100644 journal/middleware/doc.go delete mode 100644 journal/middleware/logging.go delete mode 100644 journal/middleware/metrics.go delete mode 100644 journal/middleware/tracing.go delete mode 100644 journal/mocks/doc.go delete mode 100644 journal/mocks/repository.go delete mode 100644 journal/mocks/service.go delete mode 100644 journal/postgres/doc.go delete mode 100644 journal/postgres/errors.go delete mode 100644 journal/postgres/init.go delete mode 100644 journal/postgres/journal.go delete mode 100644 journal/postgres/journal_test.go delete mode 100644 journal/postgres/setup_test.go delete mode 100644 journal/postgres/telemetry.go delete mode 100644 journal/service.go delete mode 100644 journal/service_test.go delete mode 100644 notifications/README.md delete mode 100644 notifications/doc.go delete mode 100644 notifications/emailer/atom_users.go delete mode 100644 notifications/emailer/doc.go delete mode 100644 notifications/emailer/emailer.go delete mode 100644 notifications/emailer/emailer_test.go delete mode 100644 notifications/events/consumer.go delete mode 100644 notifications/events/consumer_test.go delete mode 100644 notifications/events/doc.go delete mode 100644 notifications/middleware/doc.go delete mode 100644 notifications/middleware/logging.go delete mode 100644 notifications/middleware/logging_test.go delete mode 100644 notifications/middleware/metrics.go delete mode 100644 notifications/middleware/metrics_test.go delete mode 100644 notifications/middleware/tracing.go delete mode 100644 notifications/mocks/doc.go delete mode 100644 notifications/mocks/notifier.go delete mode 100644 notifications/notifier.go rename {internal => pkg}/atom/authz.go (100%) rename {internal => pkg}/atom/authz_compat.go (100%) rename {internal => pkg}/atom/authz_test.go (98%) rename {internal => pkg}/atom/bootstrap.go (100%) rename {internal => pkg}/atom/bootstrap_test.go (100%) rename {internal => pkg}/atom/client.go (100%) rename {internal => pkg}/atom/client_test.go (100%) rename {internal => pkg}/atom/config.go (100%) rename {internal => pkg}/atom/constants.go (98%) create mode 100644 pkg/atom/doc.go rename {internal => pkg}/atom/grpc_compat.go (100%) rename {internal => pkg}/atom/grpc_compat_test.go (100%) rename {internal => pkg}/atom/mapping.go (100%) rename {internal => pkg}/atom/mapping_test.go (100%) rename {internal => pkg}/atom/policy.go (100%) rename {internal => pkg}/atom/policy_service.go (100%) rename {internal => pkg}/atom/policy_service_test.go (100%) rename {internal => pkg}/atom/policy_test.go (96%) rename {internal => pkg}/atom/projector.go (100%) rename {internal => pkg}/atom/service_tokens.go (96%) rename {internal => pkg}/atom/service_tokens_test.go (98%) rename {internal => pkg}/atom/test_constants_test.go (100%) rename {internal => pkg}/atom/token.go (100%) rename {internal => pkg}/atom/token_test.go (100%) rename {internal => pkg}/atom/types.go (100%) rename {internal => pkg}/email/README.md (100%) rename {internal => pkg}/email/doc.go (100%) rename {internal => pkg}/email/email.go (100%) rename {consumers/writers/brokers => pkg/messaging/writers}/brokers_fluxmq.go (98%) rename {consumers/writers/brokers => pkg/messaging/writers}/brokers_nats.go (98%) delete mode 100644 pkg/re/events/consumer/decode.go delete mode 100644 pkg/re/events/consumer/doc.go delete mode 100644 pkg/re/events/consumer/stream.go create mode 100644 pkg/readersclient/client.go create mode 100644 pkg/readersclient/client_test.go delete mode 100644 pkg/sdk/journal.go rename {internal => pkg}/testsutil/common.go (100%) rename {internal => pkg}/testsutil/redis.go (100%) delete mode 100644 re/README.md delete mode 100644 re/api/doc.go delete mode 100644 re/api/endpoints.go delete mode 100644 re/api/endpoints_test.go delete mode 100644 re/api/requests.go delete mode 100644 re/api/responses.go delete mode 100644 re/api/transport.go delete mode 100644 re/atom.go delete mode 100644 re/atom_test.go delete mode 100644 re/doc.go delete mode 100644 re/events/doc.go delete mode 100644 re/events/events.go delete mode 100644 re/events/streams.go delete mode 100644 re/events/streams_test.go delete mode 100644 re/golang.go delete mode 100644 re/handlers.go delete mode 100644 re/lua.go delete mode 100644 re/middleware/authorization.go delete mode 100644 re/middleware/authorization_test.go delete mode 100644 re/middleware/callout.go delete mode 100644 re/middleware/logging.go delete mode 100644 re/middleware/metrics.go delete mode 100644 re/middleware/tracing.go delete mode 100644 re/mocks/repository.go delete mode 100644 re/mocks/service.go delete mode 100644 re/operations/operations.go delete mode 100644 re/outputs/alarm.go delete mode 100644 re/outputs/channel.go delete mode 100644 re/outputs/doc.go delete mode 100644 re/outputs/email.go delete mode 100644 re/outputs/outputs.go delete mode 100644 re/outputs/postgres.go delete mode 100644 re/outputs/senml.go delete mode 100644 re/outputs/slack.go delete mode 100644 re/postgres/init.go delete mode 100644 re/postgres/repository.go delete mode 100644 re/postgres/repository_test.go delete mode 100644 re/postgres/rule.go delete mode 100644 re/postgres/setup_test.go delete mode 100644 re/rule.go delete mode 100644 re/service.go delete mode 100644 re/service_test.go delete mode 100644 re/status.go delete mode 100644 re/status_test.go delete mode 100644 reports/README.md delete mode 100644 reports/api/doc.go delete mode 100644 reports/api/endpoints.go delete mode 100644 reports/api/endpoints_test.go delete mode 100644 reports/api/request.go delete mode 100644 reports/api/response.go delete mode 100644 reports/api/transport.go delete mode 100644 reports/atom.go delete mode 100644 reports/events/doc.go delete mode 100644 reports/events/events.go delete mode 100644 reports/events/streams.go delete mode 100644 reports/events/streams_test.go delete mode 100644 reports/generator.go delete mode 100644 reports/handler.go delete mode 100644 reports/middleware/authorization.go delete mode 100644 reports/middleware/authorization_test.go delete mode 100644 reports/middleware/callout.go delete mode 100644 reports/middleware/logging.go delete mode 100644 reports/middleware/metrics.go delete mode 100644 reports/middleware/tracing.go delete mode 100644 reports/mocks/repository.go delete mode 100644 reports/mocks/service.go delete mode 100644 reports/operations/operations.go delete mode 100644 reports/postgres/errors.go delete mode 100644 reports/postgres/init.go delete mode 100644 reports/postgres/reports.go delete mode 100644 reports/postgres/repository.go delete mode 100644 reports/postgres/repository_test.go delete mode 100644 reports/postgres/setup_test.go delete mode 100644 reports/reports.go delete mode 100644 reports/service.go delete mode 100644 reports/service_test.go delete mode 100644 reports/status.go delete mode 100644 reports/template.go delete mode 100644 reports/template_test.go delete mode 100644 reports/tz.go diff --git a/.github/workflows/api-tests.yaml b/.github/workflows/api-tests.yaml index 9d116a4b5..58a031032 100644 --- a/.github/workflows/api-tests.yaml +++ b/.github/workflows/api-tests.yaml @@ -15,14 +15,10 @@ on: - "clients/api/http/**" - "domains/api/http/**" - "groups/api/http/**" - - "journal/api/**" - "users/api/**" - "bootstrap/api/**" - "certs/api/http/**" - "readers/api/http/**" - - "re/**" - - "alarms/**" - - "reports/**" - "apidocs/openapi/**" pull_request: branches: @@ -35,14 +31,10 @@ on: - "clients/api/http/**" - "domains/api/http/**" - "groups/api/http/**" - - "journal/api/**" - "users/api/**" - "bootstrap/api/**" - "certs/api/http/**" - "readers/api/http/**" - - "re/**" - - "alarms/**" - - "reports/**" - "apidocs/openapi/**" concurrency: @@ -59,13 +51,9 @@ env: CLIENTS_URL: http://localhost CHANNELS_URL: http://localhost GROUPS_URL: http://localhost - JOURNAL_URL: http://localhost:9021 BOOTSTRAP_URL: http://localhost:9013 CERTS_URL: http://localhost:9019 READERS_URL: http://localhost:9011 - RE_URL: http://localhost:9008 - ALARMS_URL: http://localhost:8050 - REPORTS_URL: http://localhost:9017 jobs: api-test: @@ -90,10 +78,6 @@ jobs: workflow: - ".github/workflows/api-tests.yaml" - journal: - - "apidocs/openapi/journal.yaml" - - "journal/api/**" - domains: - "apidocs/openapi/domains.yaml" - "internal/atom/**" @@ -131,24 +115,6 @@ jobs: - "apidocs/openapi/readers.yaml" - "readers/api/http/**" - re: - - "apidocs/openapi/rules.yaml" - - "re/**" - - "cmd/re/**" - - "internal/atom/**" - - alarms: - - "apidocs/openapi/alarms.yaml" - - "alarms/**" - - "cmd/alarms/**" - - "internal/atom/**" - - reports: - - "apidocs/openapi/reports.yaml" - - "reports/**" - - "cmd/reports/**" - - "internal/atom/**" - - name: Build images run: make all -j $(nproc) && make dockers_dev -j $(nproc) @@ -240,17 +206,6 @@ jobs: coverage-artifact-name: schema-coverage-domains coverage-pr-comment: false - - name: Run Journal API tests - if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true' - uses: schemathesis/action@v3.0.0 - with: - schema: apidocs/openapi/journal.yaml - base-url: ${{ env.JOURNAL_URL }} - checks: all - args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' - coverage-artifact-name: schema-coverage-journal - coverage-pr-comment: false - - name: Run Bootstrap API tests if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != '' uses: schemathesis/action@v3.0.0 @@ -284,39 +239,6 @@ jobs: coverage-artifact-name: schema-coverage-readers coverage-pr-comment: false - - name: Run Rules Engine API tests - if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true' - uses: schemathesis/action@v3.0.0 - with: - schema: apidocs/openapi/rules.yaml - base-url: ${{ env.RE_URL }} - checks: all - args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' - coverage-artifact-name: schema-coverage-rules - coverage-pr-comment: false - - - name: Run Alarms API tests - if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true' - uses: schemathesis/action@v3.0.0 - with: - schema: apidocs/openapi/alarms.yaml - base-url: ${{ env.ALARMS_URL }} - checks: all - args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' - coverage-artifact-name: schema-coverage-alarms - coverage-pr-comment: false - - - name: Run Reports API tests - if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true' - uses: schemathesis/action@v3.0.0 - with: - schema: apidocs/openapi/reports.yaml - base-url: ${{ env.REPORTS_URL }} - checks: all - args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' - coverage-artifact-name: schema-coverage-reports - coverage-pr-comment: false - - name: Stop containers if: always() run: make run_latest_ci down args="-v" && make run_addons down args="-v" diff --git a/.github/workflows/check-generated-files.yaml b/.github/workflows/check-generated-files.yaml index c395d0f37..1897d5977 100644 --- a/.github/workflows/check-generated-files.yaml +++ b/.github/workflows/check-generated-files.yaml @@ -68,7 +68,6 @@ jobs: - "pkg/groups/groups.go" - "users/emailer.go" - "users/hasher.go" - - "journal/journal.go" - "consumers/notifier.go" - name: Install Protoc diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 667451a48..48ddcb5a9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -105,14 +105,22 @@ jobs: internal: - "internal/**" - journal: - - "journal/**" - - "cmd/journal/**" - - "pkg/events/**" - logger: - "logger/**" + pkg-atom: + - "pkg/atom/**" + + pkg-email: + - "pkg/email/**" + - "pkg/emailer/**" + + pkg-readersclient: + - "pkg/readersclient/**" + + pkg-testsutil: + - "pkg/testsutil/**" + pkg-errors: - "pkg/errors/**" @@ -136,11 +144,7 @@ jobs: - "channels/**" - "domains/**" - "groups/**" - - "journal/**" - "api/http/**" - - "re/**" - - "alarms/**" - - "reports/**" pkg-transformers: - "pkg/transformers/**" @@ -157,12 +161,6 @@ jobs: - "pkg/uuid/**" - "pkg/events/**" - notifications: - - "notifications/**" - - "cmd/notifications/**" - - "consumers/notifier.go" - - "pkg/events/**" - api: - "api/**" @@ -178,27 +176,14 @@ jobs: - "cmd/postgres-reader/**" - "cmd/timescale-reader/**" - re: - - "re/**" - - "cmd/re/**" - - "re/api/**" - - alarms: - - "alarms/**" - - "cmd/alarms/**" - - reports: - - "reports/**" - - "cmd/reports/**" - - name: Set matrix for changed modules id: set-matrix run: | modules=() - if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then - # If workflow or pkg/errors changed, test everything - modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports") + if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" || "${{ steps.changes.outputs.pkg-testsutil }}" == "true" ]]; then + # If workflow, shared errors, or test utilities changed, test everything + modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "logger" "pkg-atom" "pkg-email" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-readersclient" "pkg-sdk" "pkg-testsutil" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "api" "consumers" "readers") else # Add only changed modules [[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth") @@ -208,24 +193,22 @@ jobs: [[ "${{ steps.changes.outputs.domains }}" == "true" ]] && modules+=("domains") [[ "${{ steps.changes.outputs.groups }}" == "true" ]] && modules+=("groups") [[ "${{ steps.changes.outputs.internal }}" == "true" ]] && modules+=("internal") - [[ "${{ steps.changes.outputs.journal }}" == "true" ]] && modules+=("journal") [[ "${{ steps.changes.outputs.logger }}" == "true" ]] && modules+=("logger") + [[ "${{ steps.changes.outputs.pkg-atom }}" == "true" ]] && modules+=("pkg-atom") + [[ "${{ steps.changes.outputs.pkg-email }}" == "true" ]] && modules+=("pkg-email") [[ "${{ steps.changes.outputs.pkg-errors }}" == "true" ]] && modules+=("pkg-errors") [[ "${{ steps.changes.outputs.pkg-events }}" == "true" ]] && modules+=("pkg-events") [[ "${{ steps.changes.outputs.pkg-grpcclient }}" == "true" ]] && modules+=("pkg-grpcclient") [[ "${{ steps.changes.outputs.pkg-messaging }}" == "true" ]] && modules+=("pkg-messaging") + [[ "${{ steps.changes.outputs.pkg-readersclient }}" == "true" ]] && modules+=("pkg-readersclient") [[ "${{ steps.changes.outputs.pkg-sdk }}" == "true" ]] && modules+=("pkg-sdk") [[ "${{ steps.changes.outputs.pkg-transformers }}" == "true" ]] && modules+=("pkg-transformers") [[ "${{ steps.changes.outputs.pkg-ulid }}" == "true" ]] && modules+=("pkg-ulid") [[ "${{ steps.changes.outputs.pkg-uuid }}" == "true" ]] && modules+=("pkg-uuid") [[ "${{ steps.changes.outputs.users }}" == "true" ]] && modules+=("users") - [[ "${{ steps.changes.outputs.notifications }}" == "true" ]] && modules+=("notifications") [[ "${{ steps.changes.outputs.api }}" == "true" ]] && modules+=("api") [[ "${{ steps.changes.outputs.consumers }}" == "true" ]] && modules+=("consumers") [[ "${{ steps.changes.outputs.readers }}" == "true" ]] && modules+=("readers") - [[ "${{ steps.changes.outputs.re }}" == "true" ]] && modules+=("re") - [[ "${{ steps.changes.outputs.alarms }}" == "true" ]] && modules+=("alarms") - [[ "${{ steps.changes.outputs.reports }}" == "true" ]] && modules+=("reports") fi # Convert to JSON array @@ -263,11 +246,15 @@ jobs: run: | # Map module names to directories case "${{ matrix.module }}" in + pkg-atom) dir="pkg/atom" ;; + pkg-email) dir="pkg/email" ;; pkg-errors) dir="pkg/errors" ;; pkg-events) dir="pkg/events" ;; pkg-grpcclient) dir="pkg/grpcclient" ;; pkg-messaging) dir="pkg/messaging" ;; + pkg-readersclient) dir="pkg/readersclient" ;; pkg-sdk) dir="pkg/sdk" ;; + pkg-testsutil) dir="pkg/testsutil" ;; pkg-transformers) dir="pkg/transformers" ;; pkg-ulid) dir="pkg/ulid" ;; pkg-uuid) dir="pkg/uuid" ;; diff --git a/Makefile b/Makefile index ec14f98e0..104e9d4c0 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala BUILD_DIR ?= build -SERVICES = atom-bootstrap notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader alarms reports journal fluxmq -TEST_API_SERVICES = journal certs clients users channels groups domains +SERVICES = atom-bootstrap certs postgres-writer postgres-reader timescale-writer timescale-reader fluxmq +TEST_API_SERVICES = certs clients users channels groups domains TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES)) DOCKERS = $(addprefix docker_,$(SERVICES)) DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES)) @@ -24,7 +24,7 @@ DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | sed -E 's/[^a-z DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart DEFAULT_DOCKER_COMPOSE_COMMAND := up ATOM_TOKENS_ENV ?= docker/.env.tokens -REQUIRED_ATOM_TOKEN_ENVS := MG_ATOM_TOKEN_FLUXMQ_AUTH MG_ATOM_TOKEN_FLUXMQ_NODE1 MG_ATOM_TOKEN_FLUXMQ_NODE2 MG_ATOM_TOKEN_FLUXMQ_NODE3 MG_ATOM_TOKEN_JOURNAL MG_ATOM_TOKEN_NOTIFICATIONS MG_ATOM_TOKEN_TIMESCALE_READER MG_ATOM_TOKEN_RE MG_ATOM_TOKEN_ALARMS MG_ATOM_TOKEN_REPORTS MG_ATOM_TOKEN_POSTGRES_READER +REQUIRED_ATOM_TOKEN_ENVS := MG_ATOM_TOKEN_FLUXMQ_AUTH MG_ATOM_TOKEN_FLUXMQ_NODE1 MG_ATOM_TOKEN_FLUXMQ_NODE2 MG_ATOM_TOKEN_FLUXMQ_NODE3 MG_ATOM_TOKEN_TIMESCALE_READER MG_ATOM_TOKEN_RE MG_ATOM_TOKEN_ALARMS MG_ATOM_TOKEN_REPORTS MG_ATOM_TOKEN_POSTGRES_READER PROVISION_ATOM_TOKENS ?= false PROVISION_ATOM_TOKEN_GOALS := provision-atom-tokens DOCKER_BASE_ENV_FILES := --env-file docker/.env @@ -258,7 +258,6 @@ test_api_domains: TEST_API_URL := http://localhost:9000 test_api_channels: TEST_API_URL := http://localhost:9000 test_api_groups: TEST_API_URL := http://localhost:9000 test_api_certs: TEST_API_URL := http://localhost:9019 -test_api_journal: TEST_API_URL := http://localhost:9021 $(TEST_API): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/alarms/README.md b/alarms/README.md deleted file mode 100644 index 08356a556..000000000 --- a/alarms/README.md +++ /dev/null @@ -1,193 +0,0 @@ -# Alarms - -The Alarms service stores, manages and exposes alarms raised by rules and device activity. It consumes alarm events from the message broker, persists them to PostgreSQL, and provides an HTTP API for listing, viewing, updating, and deleting alarms with full authn/authz, metrics, and tracing support. - -## Configuration - -The service is configured using the following environment variables (values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) as consumed by [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml)): - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_ALARMS_LOG_LEVEL` | Log level for the service | `debug` | -| `MG_ALARMS_HTTP_HOST` | HTTP host to bind | `alarms` | -| `MG_ALARMS_HTTP_PORT` | HTTP port to bind | `8050` | -| `MG_ALARMS_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" | -| `MG_ALARMS_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" | -| `MG_ALARMS_DB_HOST` | PostgreSQL host | `alarms-db` | -| `MG_ALARMS_DB_PORT` | PostgreSQL port | `5432` | -| `MG_ALARMS_DB_USER` | PostgreSQL user | `magistrala` | -| `MG_ALARMS_DB_PASS` | PostgreSQL password | `magistrala` | -| `MG_ALARMS_DB_NAME` | PostgreSQL database name | `alarms` | -| `MG_ALARMS_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` | -| `MG_ALARMS_DB_SSL_CERT` | PostgreSQL SSL client cert | "" | -| `MG_ALARMS_DB_SSL_KEY` | PostgreSQL SSL client key | "" | -| `MG_ALARMS_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" | -| `MG_ALARMS_INSTANCE_ID` | Instance ID for tracing/health | "" | -| `MG_MESSAGE_BROKER_URL` | Message broker URL for alarm ingestion | `nats://nats:4222` | -| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` | -| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` | -| `ATOM_URL` | Atom HTTP endpoint | `http://atom:8080` | -| `ATOM_JWKS_URL` | Atom JWKS endpoint for JWT verification | `http://atom:8080/.well-known/jwks.json` | -| `ATOM_SERVICE_TOKEN` | Atom service token for authorization checks | "" | -| `ATOM_ADMIN_USERNAME` | Atom admin login fallback when no service token is configured | `atom-admin` | -| `ATOM_ADMIN_SECRET` | Atom admin secret fallback when no service token is configured | `change-me` | -| `ATOM_TIMEOUT` | Atom request timeout | `5s` | -| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` | - -## Features - -- **Alarm ingestion**: Consumes alarms from the message broker and persists them to PostgreSQL. -- **Stateful updates**: Updates assignee, acknowledgment, resolution, and metadata fields. -- **Filtering and paging**: Lists alarms by domain, rule, channel, client, subtopic, status, severity, and time range. -- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support. -- **Auth and authorization**: Authn/authz enforced through Atom JWT verification and PDP checks while alarm records stay in PostgreSQL. - -## Architecture - -### Runtime flow - -1. The message broker publishes alarm events under the `alarms.>` subject. -2. The Alarms consumer decodes the event payload, enriches it with message metadata, validates it, and calls `CreateAlarm`. -3. The repository writes to PostgreSQL while deduplicating repeated active alarms with the same severity. -4. The HTTP API exposes list/view/update/delete operations with authn/authz, metrics, and tracing middleware. - -### Components - -- **HTTP API**: `alarms/api` exposes REST endpoints and health/metrics handlers. -- **Service layer**: `alarms/service.go` validates requests and coordinates repository operations. -- **Repository**: `alarms/postgres/alarms.go` implements persistence and filtering. -- **Consumer**: `alarms/consumer` processes broker messages and creates alarms. -- **Message broker**: `alarms/brokers` uses NATS JetStream with stream `alarms` and subject `alarms.>`. -- **Migrations**: `alarms/postgres/init.go` defines the alarms schema and indexes. - -### Alarms table - -Defined in `alarms/postgres/init.go`: - -| Column | Type | Description | -| --- | --- | --- | -| `id` | `VARCHAR(36)` | Alarm UUID (primary key) | -| `rule_id` | `VARCHAR(36)` | Rule ID that triggered the alarm | -| `domain_id` | `VARCHAR(36)` | Domain ID | -| `channel_id` | `VARCHAR(36)` | Channel ID | -| `subtopic` | `TEXT` | Subtopic associated with the alarm | -| `client_id` | `VARCHAR(36)` | Client ID | -| `measurement` | `TEXT` | Measurement name | -| `value` | `TEXT` | Measured value | -| `unit` | `TEXT` | Measurement unit | -| `threshold` | `TEXT` | Threshold value | -| `cause` | `TEXT` | Cause/description | -| `status` | `SMALLINT` | 0 = active, 1 = cleared | -| `severity` | `SMALLINT` | Severity (0-100) | -| `assignee_id` | `VARCHAR(36)` | Assignee ID | -| `created_at` | `TIMESTAMPTZ` | Creation timestamp | -| `updated_at` | `TIMESTAMPTZ` | Last update timestamp | -| `updated_by` | `VARCHAR(36)` | User who updated | -| `assigned_at` | `TIMESTAMPTZ` | When assigned | -| `assigned_by` | `VARCHAR(36)` | Who assigned | -| `acknowledged_at` | `TIMESTAMPTZ` | When acknowledged | -| `acknowledged_by` | `VARCHAR(36)` | Who acknowledged | -| `resolved_at` | `TIMESTAMPTZ` | When resolved | -| `resolved_by` | `VARCHAR(36)` | Who resolved | -| `metadata` | `JSONB` | Custom metadata | - -Index: `idx_alarms_state (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC)` - -## Deployment - -### Build and run locally - -```bash -make alarms - -MG_ALARMS_LOG_LEVEL=debug \ -MG_ALARMS_HTTP_PORT=8050 \ -MG_ALARMS_DB_HOST=localhost \ -MG_ALARMS_DB_PORT=5432 \ -MG_ALARMS_DB_USER=magistrala \ -MG_ALARMS_DB_PASS=magistrala \ -MG_ALARMS_DB_NAME=alarms \ -MG_MESSAGE_BROKER_URL=nats://localhost:4222 \ -MG_AUTH_GRPC_URL=localhost:7001 \ -MG_AUTH_GRPC_TIMEOUT=300s \ -MG_DOMAINS_GRPC_URL=localhost:7003 \ -MG_DOMAINS_GRPC_TIMEOUT=300s \ -./build/alarms -``` - -### Docker Compose - -The service is available as a Docker container. Refer to [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) for the `alarms` and `alarms-db` services and their environment variables. For a full local stack, make sure the auth, domains, and message broker services are also running. - -```bash -docker compose -f docker/docker-compose.yaml up alarms alarms-db -``` - -### Health check - -```bash -curl -X GET http://localhost:8050/health \ - -H "accept: application/health+json" -``` - -## Testing - -```bash -go test ./alarms/... -``` - -## Usage - -The Alarms service supports the following operations: - -| Operation | Method & Path | Description | -| --- | --- | --- | -| `listAlarms` | `GET /{domainID}/alarms` | List alarms with filters | -| `viewAlarm` | `GET /{domainID}/alarms/{alarmID}` | Retrieve a single alarm | -| `updateAlarm` | `PUT /{domainID}/alarms/{alarmID}` | Update alarm status/assignee/metadata | -| `deleteAlarm` | `DELETE /{domainID}/alarms/{alarmID}` | Delete an alarm | -| `health` | `GET /health` | Service health check | - -Alarm creation is driven by message broker events and is not exposed as an HTTP endpoint. - -### Example: List alarms - -```bash -curl -X GET "http://localhost:8050//alarms?limit=10&offset=0&status=active&severity=50" \ - -H "Authorization: Bearer " -``` - -### Example: View an alarm - -```bash -curl -X GET http://localhost:8050//alarms/ \ - -H "Authorization: Bearer " -``` - -### Example: Update an alarm - -```bash -curl -X PUT http://localhost:8050//alarms/ \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "status": "cleared", - "assignee_id": "", - "severity": 40, - "metadata": { "note": "cleared after inspection" } - }' -``` - -### Example: Delete an alarm - -```bash -curl -X DELETE http://localhost:8050//alarms/ \ - -H "Authorization: Bearer " -``` - -### Example: Health check - -```bash -curl -X GET http://localhost:8050/health \ - -H "accept: application/health+json" -``` diff --git a/alarms/alarms.go b/alarms/alarms.go deleted file mode 100644 index 412410994..000000000 --- a/alarms/alarms.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package alarms - -import ( - "context" - "errors" - "time" - - "github.com/absmach/magistrala/pkg/authn" -) - -const SeverityMax uint8 = 100 - -var ErrInvalidSeverity = errors.New("invalid severity. Must be between 0 and 100") - -type Metadata map[string]any - -// Alarm represents an alarm instance. -type Alarm struct { - ID string `json:"id"` - RuleID string `json:"rule_id"` - DomainID string `json:"domain_id"` - ChannelID string `json:"channel_id"` - ClientID string `json:"client_id"` - Subtopic string `json:"subtopic"` - Status Status `json:"status"` - Measurement string `json:"measurement"` - Value string `json:"value"` - Unit string `json:"unit"` - Threshold string `json:"threshold"` - Cause string `json:"cause"` - Severity uint8 `json:"severity"` - AssigneeID string `json:"assignee_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - UpdatedBy string `json:"updated_by"` - AssignedAt time.Time `json:"assigned_at,omitempty"` - AssignedBy string `json:"assigned_by,omitempty"` - AcknowledgedAt time.Time `json:"acknowledged_at,omitempty"` - AcknowledgedBy string `json:"acknowledged_by,omitempty"` - ResolvedAt time.Time `json:"resolved_at,omitempty"` - ResolvedBy string `json:"resolved_by,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` -} - -type AlarmsPage struct { - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Total uint64 `json:"total"` - Alarms []Alarm `json:"alarms"` -} - -type PageMetadata struct { - Offset uint64 `json:"offset" db:"offset"` - Limit uint64 `json:"limit" db:"limit"` - DomainID string `json:"domain_id" db:"domain_id"` - RuleID string `json:"rule_id" db:"rule_id"` - RuleIDs []string `json:"rule_ids" db:"rule_ids"` - ChannelID string `json:"channel_id" db:"channel_id"` - ClientID string `json:"client_id" db:"client_id"` - Subtopic string `json:"subtopic" db:"subtopic"` - Measurement string `json:"measurement" db:"measurement"` - Dir string `json:"dir" db:"dir"` - Order string `json:"order" db:"order"` - Status Status `json:"status" db:"status"` - CreatedFrom time.Time `json:"created_from" db:"created_from"` - CreatedTo time.Time `json:"created_to" db:"created_to"` - AssigneeID string `json:"assignee_id" db:"assignee_id"` - Severity uint8 `json:"severity" db:"severity"` - UpdatedBy string `json:"updated_by" db:"updated_by"` - AssignedBy string `json:"assigned_by" db:"assigned_by"` - AcknowledgedBy string `json:"acknowledged_by" db:"acknowledged_by"` - ResolvedBy string `json:"resolved_by" db:"resolved_by"` - UserID string `json:"user_id" db:"user_id"` -} - -func (a Alarm) Validate() error { - if a.RuleID == "" { - return errors.New("rule_id is required") - } - if a.DomainID == "" { - return errors.New("domain_id is required") - } - if a.ChannelID == "" { - return errors.New("channel_id is required") - } - if a.ClientID == "" { - return errors.New("client_id is required") - } - if a.Measurement == "" { - return errors.New("measurement is required") - } - if a.Value == "" { - return errors.New("value is required") - } - if a.Cause == "" { - return errors.New("cause is required") - } - if a.Severity > SeverityMax { - return ErrInvalidSeverity - } - - return nil -} - -// Service specifies an API that must be fulfilled by the domain service. -type Service interface { - CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) - UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error) - ViewAlarm(ctx context.Context, session authn.Session, id string) (Alarm, error) - ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) - DeleteAlarm(ctx context.Context, session authn.Session, id string) error -} - -type Repository interface { - CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) - UpdateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) - ViewAlarm(ctx context.Context, alarmID, domainID string) (Alarm, error) - ListAllAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, error) - DeleteAlarm(ctx context.Context, id string) error -} diff --git a/alarms/alarms_test.go b/alarms/alarms_test.go deleted file mode 100644 index 2fc3ef72e..000000000 --- a/alarms/alarms_test.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package alarms_test - -import ( - "fmt" - "testing" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestValidateAlarms(t *testing.T) { - cases := []struct { - desc string - alarm alarms.Alarm - err error - }{ - { - desc: "valid alarm", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: nil, - }, - { - desc: "missing rule_id", - alarm: alarms.Alarm{ - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("rule_id is required"), - }, - { - desc: "missing domain_id", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("domain_id is required"), - }, - { - desc: "missing channel_id", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("channel_id is required"), - }, - { - desc: "missing client_id", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("client_id is required"), - }, - { - desc: "missing measurement", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("measurement is required"), - }, - { - desc: "missing value", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: errors.New("value is required"), - }, - { - desc: "missing cause", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Severity: 100, - }, - err: errors.New("cause is required"), - }, - { - desc: "higher severity", - alarm: alarms.Alarm{ - RuleID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - ClientID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: alarms.SeverityMax + 1, - }, - err: alarms.ErrInvalidSeverity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.alarm.Validate() - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - }) - } -} diff --git a/alarms/api/doc.go b/alarms/api/doc.go deleted file mode 100644 index 2424852cc..000000000 --- a/alarms/api/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package api contains API-related concerns: endpoint definitions, middlewares -// and all resource representations. -package api diff --git a/alarms/api/endpoint.go b/alarms/api/endpoint.go deleted file mode 100644 index a6a98fbe1..000000000 --- a/alarms/api/endpoint.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - - "github.com/absmach/magistrala/alarms" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/go-kit/kit/endpoint" -) - -func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(updateAlarmReq) - if err := req.validate(); err != nil { - return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return alarmRes{}, svcerr.ErrAuthorization - } - - alarm, err := svc.UpdateAlarm(ctx, session, req.Alarm) - if err != nil { - return alarmRes{}, err - } - - return alarmRes{ - Alarm: alarm, - }, nil - } -} - -func viewAlarmEndpoint(svc alarms.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(alarmReq) - if err := req.validate(); err != nil { - return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return alarmRes{}, svcerr.ErrAuthorization - } - - alarm, err := svc.ViewAlarm(ctx, session, req.ID) - if err != nil { - return alarmRes{}, err - } - - return alarmRes{ - Alarm: alarm, - }, nil - } -} - -func listAlarmsEndpoint(svc alarms.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(listAlarmsReq) - if err := req.validate(); err != nil { - return alarmsPageRes{}, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return alarmsPageRes{}, svcerr.ErrAuthorization - } - - alarms, err := svc.ListAlarms(ctx, session, req.PageMetadata) - if err != nil { - return alarmsPageRes{}, err - } - - return alarmsPageRes{ - AlarmsPage: alarms, - }, nil - } -} - -func deleteAlarmEndpoint(svc alarms.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(alarmReq) - if err := req.validate(); err != nil { - return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return alarmRes{}, svcerr.ErrAuthorization - } - - if err := svc.DeleteAlarm(ctx, session, req.ID); err != nil { - return alarmRes{}, err - } - - return alarmRes{deleted: true}, nil - } -} diff --git a/alarms/api/requests.go b/alarms/api/requests.go deleted file mode 100644 index 04ca9e86d..000000000 --- a/alarms/api/requests.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "errors" - - "github.com/absmach/magistrala/alarms" - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" -) - -type alarmReq struct { - alarms.Alarm `json:",inline"` -} - -func (req alarmReq) validate() error { - if req.Alarm.ID == "" { - return errors.New("missing alarm id") - } - - return nil -} - -type updateAlarmReq struct { - alarms.Alarm `json:",inline"` -} - -func (req updateAlarmReq) validate() error { - if req.Alarm.ID == "" { - return errors.New("missing alarm id") - } - if req.Alarm.AssigneeID == "" && req.Alarm.AcknowledgedBy == "" && req.Alarm.ResolvedBy == "" && len(req.Alarm.Metadata) == 0 { - return errors.New("at least one of assignee_id, acknowledged_by, resolved_by, or metadata must be set") - } - - return nil -} - -type listAlarmsReq struct { - alarms.PageMetadata -} - -func (req listAlarmsReq) validate() error { - if req.Limit > api.MaxLimitSize || req.Limit < 1 { - return apiutil.ErrLimitSize - } - - if req.Order != "" && req.Order != api.UpdatedAtOrder && req.Order != api.CreatedAtOrder { - return apiutil.ErrInvalidOrder - } - - if req.Dir != api.AscDir && req.Dir != api.DescDir { - return apiutil.ErrInvalidDirection - } - - return nil -} diff --git a/alarms/api/responses.go b/alarms/api/responses.go deleted file mode 100644 index b36125755..000000000 --- a/alarms/api/responses.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "fmt" - "net/http" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/alarms" -) - -var ( - _ magistrala.Response = (*alarmRes)(nil) - _ magistrala.Response = (*alarmsPageRes)(nil) -) - -type alarmRes struct { - alarms.Alarm `json:",inline"` - created bool - deleted bool -} - -func (res alarmRes) Headers() map[string]string { - switch { - case res.created: - return map[string]string{ - "Location": fmt.Sprintf("/%s/alarms/%s", res.DomainID, res.ID), - } - default: - return map[string]string{} - } -} - -func (res alarmRes) Code() int { - switch { - case res.created: - return http.StatusCreated - case res.deleted: - return http.StatusNoContent - default: - return http.StatusOK - } -} - -func (res alarmRes) Empty() bool { - switch { - case res.deleted: - return true - default: - return false - } -} - -type alarmsPageRes struct { - alarms.AlarmsPage `json:",inline"` -} - -func (res alarmsPageRes) Headers() map[string]string { - return map[string]string{} -} - -func (res alarmsPageRes) Code() int { - return http.StatusOK -} - -func (res alarmsPageRes) Empty() bool { - return false -} diff --git a/alarms/api/transport.go b/alarms/api/transport.go deleted file mode 100644 index 4451dd809..000000000 --- a/alarms/api/transport.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "encoding/json" - "log/slog" - "math" - "net/http" - "strings" - "time" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/alarms" - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/go-chi/chi/v5" - kithttp "github.com/go-kit/kit/transport/http" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -func MakeHandler(svc alarms.Service, logger *slog.Logger, idp magistrala.IDProvider, instanceID string, authn smqauthn.AuthNMiddleware) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - - mux := chi.NewRouter() - - mux.Route("/{domainID}/alarms", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()) - r.Use(api.RequestIDMiddleware(idp)) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - listAlarmsEndpoint(svc), - decodeListAlarmsReq, - api.EncodeResponse, - opts..., - ), "list_alarms").ServeHTTP) - r.Route("/{alarmID}", func(r chi.Router) { - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - viewAlarmEndpoint(svc), - decodeAlarmReq, - api.EncodeResponse, - opts..., - ), "get_alarm").ServeHTTP) - r.Put("/", otelhttp.NewHandler(kithttp.NewServer( - updateAlarmEndpoint(svc), - decodeUpdateAlarmReq, - api.EncodeResponse, - opts..., - ), "update_alarm").ServeHTTP) - r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( - deleteAlarmEndpoint(svc), - decodeAlarmReq, - api.EncodeResponse, - opts..., - ), "delete_alarm").ServeHTTP) - }) - }) - }) - - mux.Get("/health", magistrala.Health("alarms", instanceID)) - mux.Handle("/metrics", promhttp.Handler()) - - return mux -} - -func decodeListAlarmsReq(_ context.Context, r *http.Request) (any, error) { - offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - domainID, err := apiutil.ReadStringQuery(r, "domain_id", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - channelID, err := apiutil.ReadStringQuery(r, "channel_id", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - clientID, err := apiutil.ReadStringQuery(r, "client_id", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - subtopic, err := apiutil.ReadStringQuery(r, "subtopic", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - ruleID, err := apiutil.ReadStringQuery(r, "rule_id", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - s, err := apiutil.ReadStringQuery(r, api.StatusKey, alarms.All) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - status, err := alarms.ToStatus(s) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - assigneeID, err := apiutil.ReadStringQuery(r, "assignee_id", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - serverity, err := apiutil.ReadNumQuery(r, "severity", uint64(math.MaxUint8)) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - updatedBy, err := apiutil.ReadStringQuery(r, "updated_by", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - assignedBy, err := apiutil.ReadStringQuery(r, "assigned_by", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - acknowledgedBy, err := apiutil.ReadStringQuery(r, "acknowledged_by", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - resolvedBy, err := apiutil.ReadStringQuery(r, "resolved_by", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - cfrom, err := apiutil.ReadStringQuery(r, "created_from", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - cto, err := apiutil.ReadStringQuery(r, "created_to", "") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder) - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - dir, err := apiutil.ReadStringQuery(r, api.DirKey, "desc") - if err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - - var createdFrom, createdTo time.Time - if cfrom != "" { - if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - } - if cto != "" { - if createdTo, err = time.Parse(time.RFC3339, cto); err != nil { - return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err) - } - } - - return listAlarmsReq{ - PageMetadata: alarms.PageMetadata{ - Offset: offset, - Limit: limit, - DomainID: domainID, - ChannelID: channelID, - ClientID: clientID, - Subtopic: subtopic, - RuleID: ruleID, - Status: status, - AssigneeID: assigneeID, - ResolvedBy: resolvedBy, - Severity: uint8(serverity), - UpdatedBy: updatedBy, - AcknowledgedBy: acknowledgedBy, - AssignedBy: assignedBy, - CreatedFrom: createdFrom, - CreatedTo: createdTo, - Dir: dir, - Order: order, - }, - }, nil -} - -func decodeAlarmReq(_ context.Context, r *http.Request) (any, error) { - return alarmReq{ - Alarm: alarms.Alarm{ - ID: chi.URLParam(r, "alarmID"), - }, - }, nil -} - -func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return updateAlarmReq{}, apiutil.ErrUnsupportedContentType - } - - req := updateAlarmReq{} - if err := json.NewDecoder(r.Body).Decode(&req.Alarm); err != nil { - return updateAlarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - - req.Alarm.ID = chi.URLParam(r, "alarmID") - - return req, nil -} diff --git a/alarms/brokers/brokers_fluxmq.go b/alarms/brokers/brokers_fluxmq.go deleted file mode 100644 index 58b5b8c17..000000000 --- a/alarms/brokers/brokers_fluxmq.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -//go:build msg_fluxmq -// +build msg_fluxmq - -package brokers - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/pkg/messaging" - broker "github.com/absmach/magistrala/pkg/messaging/fluxmq" - "github.com/nats-io/nats.go/jetstream" -) - -const ( - AllTopic = "alarms/#" - - prefix = "alarms" -) - -var cfg = jetstream.StreamConfig{ - Name: "alarms", - Description: "Magistrala stream alarms", - Subjects: []string{"alarms/#"}, - Retention: jetstream.LimitsPolicy, - MaxMsgsPerSubject: 1e6, - MaxAge: time.Hour * 24, - MaxMsgSize: 1024 * 1024, - Discard: jetstream.DiscardOld, - Storage: jetstream.FileStorage, -} - -func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) { - pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pubsub")) - if err != nil { - return nil, err - } - - return pb, nil -} - -func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) { - pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pub")) - if err != nil { - return nil, err - } - - return pb, nil -} diff --git a/alarms/brokers/brokers_nats.go b/alarms/brokers/brokers_nats.go deleted file mode 100644 index f67e054ac..000000000 --- a/alarms/brokers/brokers_nats.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -//go:build !msg_fluxmq && !msg_rabbitmq && !rabbitmq -// +build !msg_fluxmq,!msg_rabbitmq,!rabbitmq - -package brokers - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/pkg/messaging" - broker "github.com/absmach/magistrala/pkg/messaging/nats" - "github.com/nats-io/nats.go/jetstream" -) - -const ( - AllTopic = "alarms/#" - - prefix = "alarms" -) - -var cfg = jetstream.StreamConfig{ - Name: "alarms", - Description: "Magistrala stream alarms", - Subjects: []string{"alarms.>"}, - Retention: jetstream.LimitsPolicy, - MaxMsgsPerSubject: 1e6, - MaxAge: time.Hour * 24, - MaxMsgSize: 1024 * 1024, - Discard: jetstream.DiscardOld, - Storage: jetstream.FileStorage, -} - -func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) { - pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg)) - if err != nil { - return nil, err - } - - return pb, nil -} - -func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) { - pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg)) - if err != nil { - return nil, err - } - - return pb, nil -} diff --git a/alarms/consumer/consumer.go b/alarms/consumer/consumer.go deleted file mode 100644 index 216e39e61..000000000 --- a/alarms/consumer/consumer.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package consumer - -import ( - "bytes" - "context" - "encoding/gob" - "log/slog" - "time" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/messaging" -) - -var errFailedToDecode = errors.New("failed to decode alarm") - -type handler struct { - svc alarms.Service - logger *slog.Logger -} - -func NewHandler(svc alarms.Service, logger *slog.Logger) messaging.MessageHandler { - return &handler{svc: svc, logger: logger} -} - -func (h handler) Handle(msg *messaging.Message) (err error) { - if msg == nil { - return errors.New("message is empty") - } - if msg.GetPayload() == nil { - return errors.New("message payload is empty") - } - - var alarm alarms.Alarm - if err := gob.NewDecoder(bytes.NewReader(msg.GetPayload())).Decode(&alarm); err != nil { - return messaging.NewError(errors.Wrap(errFailedToDecode, err), messaging.Term) - } - alarm.DomainID = msg.GetDomain() - alarm.ChannelID = msg.GetChannel() - alarm.ClientID = msg.ClientIdentity() - alarm.Subtopic = msg.GetSubtopic() - alarm.CreatedAt = time.Unix(0, int64(msg.GetCreated())) - - if err := alarm.Validate(); err != nil { - return err - } - - _, err = h.svc.CreateAlarm(context.Background(), alarm) - return err -} - -func (h handler) Cancel() error { - return nil -} diff --git a/alarms/doc.go b/alarms/doc.go deleted file mode 100644 index 9f7866f33..000000000 --- a/alarms/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package alarms contains domain concept definitions needed to support -// Alarms service feature, i.e. create, read, update, and delete alarms. -package alarms diff --git a/alarms/middleware/authorization.go b/alarms/middleware/authorization.go deleted file mode 100644 index 9bfdfa93d..000000000 --- a/alarms/middleware/authorization.go +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/alarms/operations" - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - smqauthz "github.com/absmach/magistrala/pkg/authz" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/policies" -) - -var ( - errDomainUpdateAlarms = errors.New("not authorized to update alarms in domain") - errDomainDeleteAlarms = errors.New("not authorized to delete alarms in domain") - errDomainViewAlarms = errors.New("not authorized to view alarms in domain") -) - -type authorizationMiddleware struct { - svc alarms.Service - authz smqauthz.Authorization - atomAuthz atom.Authorizer - entitiesOps permissions.EntitiesOperations[permissions.Operation] -} - -var _ alarms.Service = (*authorizationMiddleware)(nil) - -const ( - atomObjectKindResource = "resource" - atomObjectTypeResourceRule = "resource:" + atom.KindRule - atomAuthorizedRulePageLimit = 100 -) - -type atomAuthorizedObjectLister interface { - AuthorizedObjectIDs(ctx context.Context, q atom.AuthorizedObjectIDsQuery) (atom.AuthorizedObjectIDs, error) -} - -func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - - return &authorizationMiddleware{ - svc: svc, - authz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func NewAtomAuthorizationMiddleware(svc alarms.Service, authz atom.Authorizer, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - - return &authorizationMiddleware{ - svc: svc, - atomAuthz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func (am *authorizationMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - return am.svc.CreateAlarm(ctx, alarm) -} - -func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) { - current, err := am.svc.ViewAlarm(ctx, session, alarm.ID) - if err != nil { - return alarms.Alarm{}, err - } - - if len(alarm.Metadata) > 0 { - if err := am.authorizeAlarmOrRule(ctx, operations.OpUpdateAlarm, session, current); err != nil { - return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err) - } - } - - if alarm.AssigneeID != "" { - if err := am.authorizeAlarmOrRule(ctx, operations.OpAssignAlarm, session, current); err != nil { - return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err) - } - if am.atomAuthz == nil { - domainUserID := auth.EncodeDomainUserID(session.DomainID, alarm.AssigneeID) - if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: domainUserID, - Permission: policies.MembershipPermission, - ObjectType: policies.DomainType, - Object: session.DomainID, - }, nil); err != nil { - return alarms.Alarm{}, err - } - } - } - - if alarm.AcknowledgedBy != "" { - if err := am.authorizeAlarmOrRule(ctx, operations.OpAcknowledgeAlarm, session, current); err != nil { - return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err) - } - } - - if alarm.ResolvedBy != "" { - if err := am.authorizeAlarmOrRule(ctx, operations.OpResolveAlarm, session, current); err != nil { - return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err) - } - } - - return am.svc.UpdateAlarm(ctx, session, alarm) -} - -func (am *authorizationMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error { - alarm, err := am.svc.ViewAlarm(ctx, session, id) - if err != nil { - return err - } - if err := am.authorizeAlarmOrRule(ctx, operations.OpDeleteAlarm, session, alarm); err != nil { - return errors.Wrap(errDomainDeleteAlarms, err) - } - - return am.svc.DeleteAlarm(ctx, session, id) -} - -func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - if pm.DomainID == "" { - pm.DomainID = session.DomainID - } - - switch err := am.checkSuperAdmin(ctx, session); { - case err == nil: - session.SuperAdmin = true - case errors.Contains(err, svcerr.ErrSuperAdminAction): - if err := am.authorizeTenantAlarm(ctx, operations.OpViewAlarm, session); err != nil { - if pm.RuleID != "" { - if ruleErr := am.authorizeRuleAlarmRead(ctx, session, pm.RuleID); ruleErr != nil { - return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err) - } - break - } - ruleIDs, ruleErr := am.authorizedReadableRuleIDs(ctx, session) - if ruleErr != nil { - return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err) - } - if len(ruleIDs) == 0 { - return alarms.AlarmsPage{ - Offset: pm.Offset, - Limit: pm.Limit, - Alarms: []alarms.Alarm{}, - }, nil - } - pm.RuleIDs = ruleIDs - } - default: - return alarms.AlarmsPage{}, err - } - - return am.svc.ListAlarms(ctx, session, pm) -} - -func (am *authorizationMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) { - alarm, err := am.svc.ViewAlarm(ctx, session, id) - if err != nil { - return alarms.Alarm{}, err - } - if err := am.authorizeViewAlarm(ctx, session, alarm); err != nil { - return alarms.Alarm{}, errors.Wrap(errDomainViewAlarms, err) - } - - return alarm, nil -} - -func (am *authorizationMiddleware) authorizeAlarmOrRule(ctx context.Context, op permissions.Operation, session authn.Session, alarm alarms.Alarm) error { - tenantErr := am.authorizeTenantAlarm(ctx, op, session) - if tenantErr == nil { - return nil - } - if alarm.RuleID == "" { - return tenantErr - } - if err := am.authorize(ctx, op, session, policies.RulesType, alarm.RuleID, atom.KindRule); err != nil { - return tenantErr - } - return nil -} - -func (am *authorizationMiddleware) authorizeViewAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) error { - tenantErr := am.authorizeTenantAlarm(ctx, operations.OpViewAlarm, session) - if tenantErr == nil { - return nil - } - if alarm.RuleID == "" { - return tenantErr - } - if err := am.authorizeRuleAlarmRead(ctx, session, alarm.RuleID); err != nil { - return tenantErr - } - return nil -} - -func (am *authorizationMiddleware) authorizeTenantAlarm(ctx context.Context, op permissions.Operation, session authn.Session) error { - return am.authorize(ctx, op, session, policies.DomainType, session.DomainID, atom.KindAlarm) -} - -func (am *authorizationMiddleware) authorizeRuleAlarmRead(ctx context.Context, session authn.Session, ruleID string) error { - if am.atomAuthz != nil { - return am.authorize(ctx, operations.OpViewAlarm, session, policies.RulesType, ruleID, atom.KindRule) - } - perm, err := am.entitiesOps.GetPermission(operations.EntityType, operations.OpViewAlarm) - if err != nil { - return err - } - pr := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: ruleID, - ObjectType: policies.RulesType, - Permission: perm.String(), - } - return am.authz.Authorize(ctx, pr, nil) -} - -func (am *authorizationMiddleware) authorizedReadableRuleIDs(ctx context.Context, session authn.Session) ([]string, error) { - lister, ok := am.atomAuthz.(atomAuthorizedObjectLister) - if !ok { - return nil, errors.ErrAuthorization - } - perm, err := am.entitiesOps.GetPermission(operations.EntityType, operations.OpViewAlarm) - if err != nil { - return nil, err - } - - var ids []string - for offset := uint64(0); ; offset += atomAuthorizedRulePageLimit { - page, err := lister.AuthorizedObjectIDs(ctx, atom.AuthorizedObjectIDsQuery{ - SubjectID: atom.SubjectID(session), - Action: atom.CapabilityName(perm.String()), - ObjectKind: atomObjectKindResource, - ObjectType: atomObjectTypeResourceRule, - TenantID: session.DomainID, - Limit: atomAuthorizedRulePageLimit, - Offset: offset, - }) - if err != nil { - return nil, err - } - - ids = append(ids, page.IDs...) - if uint64(len(page.IDs)) < atomAuthorizedRulePageLimit || offset+uint64(len(page.IDs)) >= page.Total { - break - } - } - return ids, nil -} - -func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj, resourceKind string) error { - perm, err := am.entitiesOps.GetPermission(operations.EntityType, op) - if err != nil { - return err - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, perm.String(), objType, obj, resourceKind) - } - - pr := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: obj, - ObjectType: objType, - Permission: perm.String(), - } - - var pat *smqauthz.PATReq - if session.PatID != "" { - opName := am.entitiesOps.OperationName(operations.EntityType, op) - pat = &smqauthz.PATReq{ - UserID: session.UserID, - PatID: session.PatID, - EntityID: auth.AnyIDs, - EntityType: auth.RulesType.String(), - Operation: opName, - Domain: session.DomainID, - } - } - - if err := am.authz.Authorize(ctx, pr, pat); err != nil { - return err - } - - return nil -} - -func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { - if session.Role != authn.SuperAdminRole { - return svcerr.ErrSuperAdminAction - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, policies.AdminPermission, policies.PlatformType, policies.MagistralaObject, policies.PlatformType) - } - if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{ - SubjectType: policies.UserType, - Subject: session.UserID, - Permission: policies.AdminPermission, - ObjectType: policies.PlatformType, - Object: policies.MagistralaObject, - }, nil); err != nil { - return err - } - return nil -} diff --git a/alarms/middleware/authorization_test.go b/alarms/middleware/authorization_test.go deleted file mode 100644 index 1e67cd363..000000000 --- a/alarms/middleware/authorization_test.go +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "testing" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/alarms/mocks" - "github.com/absmach/magistrala/alarms/operations" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type recordingAtomAuthorizer struct { - allowed bool - allow func(atom.AuthzRequest) bool - authorized atom.AuthorizedObjectIDs - reqs []atom.AuthzRequest - queries []atom.AuthorizedObjectIDsQuery -} - -func (a *recordingAtomAuthorizer) CheckAuthz(_ context.Context, req atom.AuthzRequest) (atom.AuthzResponse, error) { - a.reqs = append(a.reqs, req) - if a.allow != nil { - return atom.AuthzResponse{Allowed: a.allow(req)}, nil - } - return atom.AuthzResponse{Allowed: a.allowed}, nil -} - -func (a *recordingAtomAuthorizer) AuthorizedObjectIDs(_ context.Context, q atom.AuthorizedObjectIDsQuery) (atom.AuthorizedObjectIDs, error) { - a.queries = append(a.queries, q) - return a.authorized, nil -} - -func TestListAlarmsAuthorizesTenantAlarmReader(t *testing.T) { - svc := mocks.NewService(t) - pm := alarms.PageMetadata{Limit: 10} - expectedPM := pm - expectedPM.DomainID = "domain-1" - session := authn.Session{UserID: "user-1", DomainID: "domain-1", DomainUserID: "domain-1_user-1"} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListAlarms", mock.Anything, session, expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once() - page, err := wrapped.ListAlarms(context.Background(), session, pm) - - require.NoError(t, err) - assert.Equal(t, uint64(10), page.Limit) - require.Len(t, authz.reqs, 1) - assert.Equal(t, atom.AuthzRequest{ - SubjectID: "user-1", - Action: "alarm_read", - ResourceID: "", - ObjectKind: "tenant", - ObjectID: "domain-1", - Context: map[string]any{ - "domain_id": "domain-1", - "legacy_object_type": "domain", - }, - }, authz.reqs[0]) -} - -func TestListAlarmsFiltersToReadableRulesWhenTenantAlarmReadDenied(t *testing.T) { - svc := mocks.NewService(t) - pm := alarms.PageMetadata{Limit: 10} - expectedPM := pm - expectedPM.DomainID = "domain-1" - expectedPM.RuleIDs = []string{"rule-1", "rule-2"} - authz := &recordingAtomAuthorizer{ - allowed: false, - authorized: atom.AuthorizedObjectIDs{IDs: []string{"rule-1", "rule-2"}, Total: 2}, - } - wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListAlarms", mock.Anything, authn.Session{UserID: "user-1", DomainID: "domain-1"}, expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once() - _, err = wrapped.ListAlarms(context.Background(), authn.Session{UserID: "user-1", DomainID: "domain-1"}, pm) - - require.NoError(t, err) - require.Len(t, authz.reqs, 1) - assert.Equal(t, "alarm_read", authz.reqs[0].Action) - assert.Equal(t, "tenant", authz.reqs[0].ObjectKind) - require.Len(t, authz.queries, 1) - assert.Equal(t, atom.AuthorizedObjectIDsQuery{ - SubjectID: "user-1", - Action: "alarm_read", - ObjectKind: "resource", - ObjectType: "resource:rule", - TenantID: "domain-1", - Limit: 100, - }, authz.queries[0]) -} - -func TestListAlarmsWithRuleFilterAuthorizesRuleRead(t *testing.T) { - svc := mocks.NewService(t) - pm := alarms.PageMetadata{Limit: 10, RuleID: "rule-1"} - expectedPM := pm - expectedPM.DomainID = "domain-1" - session := authn.Session{UserID: "user-1", DomainID: "domain-1"} - authz := &recordingAtomAuthorizer{ - allow: func(req atom.AuthzRequest) bool { - return req.Action == "alarm_read" && req.ObjectKind == "resource" && req.ObjectID == "rule-1" - }, - } - wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListAlarms", mock.Anything, session, expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once() - _, err = wrapped.ListAlarms(context.Background(), session, pm) - - require.NoError(t, err) - require.Len(t, authz.reqs, 2) - assert.Equal(t, "alarm_read", authz.reqs[0].Action) - assert.Equal(t, "alarm_read", authz.reqs[1].Action) - assert.Equal(t, "resource", authz.reqs[1].ObjectKind) - assert.Equal(t, "rules", authz.reqs[1].Context["legacy_object_type"]) -} - -func TestListAlarmsSuperAdminSkipsListAuthorization(t *testing.T) { - svc := mocks.NewService(t) - pm := alarms.PageMetadata{Limit: 10} - expectedPM := pm - expectedPM.DomainID = "domain-1" - session := authn.Session{UserID: "admin-1", DomainID: "domain-1", Role: authn.SuperAdminRole} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListAlarms", mock.Anything, mock.MatchedBy(func(s authn.Session) bool { - return s.SuperAdmin - }), expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once() - _, err = wrapped.ListAlarms(context.Background(), session, pm) - - require.NoError(t, err) - require.Len(t, authz.reqs, 1) - assert.Equal(t, "manage", authz.reqs[0].Action) -} - -func TestAcknowledgeAlarmAuthorizesRuleAlarmActionWhenTenantDenied(t *testing.T) { - svc := mocks.NewService(t) - session := authn.Session{UserID: "user-1", DomainID: "domain-1"} - current := alarms.Alarm{ID: "alarm-1", RuleID: "rule-1", DomainID: "domain-1"} - update := alarms.Alarm{ID: "alarm-1", AcknowledgedBy: "user-1"} - authz := &recordingAtomAuthorizer{ - allow: func(req atom.AuthzRequest) bool { - return req.Action == "alarm_acknowledge" && req.ObjectKind == "resource" && req.ObjectID == "rule-1" - }, - } - wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ViewAlarm", mock.Anything, session, "alarm-1").Return(current, nil).Once() - svc.On("UpdateAlarm", mock.Anything, session, update).Return(update, nil).Once() - _, err = wrapped.UpdateAlarm(context.Background(), session, update) - - require.NoError(t, err) - require.Len(t, authz.reqs, 2) - assert.Equal(t, atom.AuthzRequest{ - SubjectID: "user-1", - Action: "alarm_acknowledge", - ResourceID: "", - ObjectKind: "tenant", - ObjectID: "domain-1", - Context: map[string]any{ - "domain_id": "domain-1", - "legacy_object_type": "domain", - }, - }, authz.reqs[0]) - assert.Equal(t, atom.AuthzRequest{ - SubjectID: "user-1", - Action: "alarm_acknowledge", - ResourceID: "rule-1", - ObjectKind: "resource", - ObjectID: "rule-1", - Context: map[string]any{ - "domain_id": "domain-1", - "legacy_object_type": "rules", - }, - }, authz.reqs[1]) -} - -func testEntitiesOps(t *testing.T) permissions.EntitiesOperations[permissions.Operation] { - t.Helper() - details := operations.OperationDetails() - perms := make(map[string]permissions.Permission, len(details)) - for op, detail := range details { - if detail.PermissionRequired { - perms[detail.Name] = testPermission(op, detail.Name) - } - } - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{operations.EntityType: perms}, - permissions.EntitiesOperationDetails[permissions.Operation]{operations.EntityType: details}, - ) - require.NoError(t, err) - return entitiesOps -} - -func testPermission(op permissions.Operation, fallback string) permissions.Permission { - switch op { - case operations.OpViewAlarm, operations.OpListAlarms: - return "alarm_read_permission" - case operations.OpUpdateAlarm: - return "alarm_update_permission" - case operations.OpDeleteAlarm: - return "alarm_delete_permission" - case operations.OpAssignAlarm: - return "alarm_assign_permission" - case operations.OpAcknowledgeAlarm: - return "alarm_acknowledge_permission" - case operations.OpResolveAlarm: - return "alarm_resolve_permission" - default: - return permissions.Permission(fallback) - } -} diff --git a/alarms/middleware/doc.go b/alarms/middleware/doc.go deleted file mode 100644 index ce4a296d2..000000000 --- a/alarms/middleware/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package middleware provides middleware for the alarms service. -// This is logging, metrics, and tracing middleware. -package middleware diff --git a/alarms/middleware/logging.go b/alarms/middleware/logging.go deleted file mode 100644 index 0f47f57c9..000000000 --- a/alarms/middleware/logging.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/authn" - "github.com/go-chi/chi/v5/middleware" -) - -type loggingMiddleware struct { - logger *slog.Logger - service alarms.Service -} - -var _ alarms.Service = (*loggingMiddleware)(nil) - -func NewLoggingMiddleware(logger *slog.Logger, service alarms.Service) alarms.Service { - return &loggingMiddleware{ - logger: logger, - service: service, - } -} - -func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (created alarms.Alarm, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.Group("alarm", - slog.String("rule_id", alarm.RuleID), - slog.String("domain_id", alarm.DomainID), - slog.String("channel_id", alarm.ChannelID), - slog.String("client_id", alarm.ClientID), - slog.String("subtopic", alarm.Subtopic), - slog.String("measurement", alarm.Measurement), - slog.String("value", alarm.Value), - slog.String("unit", alarm.Unit), - slog.Uint64("status", uint64(alarm.Status)), - slog.Uint64("severity", uint64(alarm.Severity)), - slog.String("threshold", alarm.Threshold), - slog.String("cause", alarm.Cause), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Create alarm failed", args...) - return - } - if created.ID != "" { - lm.logger.Info("Create alarm completed successfully", args...) - } - }(time.Now()) - - return lm.service.CreateAlarm(ctx, alarm) -} - -func (lm *loggingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (dba alarms.Alarm, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.Group("alarm", - slog.String("id", dba.ID), - slog.String("rule_id", dba.RuleID), - slog.String("domain_id", dba.DomainID), - slog.String("channel_id", dba.ChannelID), - slog.String("client_id", dba.ClientID), - slog.String("subtopic", dba.Subtopic), - slog.String("measurement", dba.Measurement), - slog.String("value", dba.Value), - slog.String("unit", dba.Unit), - slog.String("status", dba.Status.String()), - slog.Uint64("severity", uint64(dba.Severity)), - slog.String("threshold", dba.Threshold), - slog.String("cause", dba.Cause), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Update alarm failed", args...) - return - } - lm.logger.Info("Update alarm completed successfully", args...) - }(time.Now()) - - return lm.service.UpdateAlarm(ctx, session, alarm) -} - -func (lm *loggingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (dba alarms.Alarm, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.String("id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("View alarm failed", args...) - return - } - lm.logger.Info("View alarm completed successfully", args...) - }(time.Now()) - - return lm.service.ViewAlarm(ctx, session, id) -} - -func (lm *loggingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (dbp alarms.AlarmsPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.Int("offset", int(pm.Offset)), - slog.Int("limit", int(pm.Limit)), - slog.String("rule_id", pm.RuleID), - slog.String("domain_id", pm.DomainID), - slog.String("channel_id", pm.ChannelID), - slog.String("client_id", pm.ClientID), - slog.String("subtopic", pm.Subtopic), - slog.String("status", pm.Status.String()), - slog.Uint64("severity", uint64(pm.Severity)), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("List alarms failed", args...) - return - } - lm.logger.Info("List alarms completed successfully", args...) - }(time.Now()) - - return lm.service.ListAlarms(ctx, session, pm) -} - -func (lm *loggingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.String("id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Delete alarm failed", args...) - return - } - lm.logger.Info("Delete alarm completed successfully", args...) - }(time.Now()) - - return lm.service.DeleteAlarm(ctx, session, id) -} diff --git a/alarms/middleware/metrics.go b/alarms/middleware/metrics.go deleted file mode 100644 index cacb8e12e..000000000 --- a/alarms/middleware/metrics.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/authn" - "github.com/go-kit/kit/metrics" -) - -type metricsMiddleware struct { - counter metrics.Counter - latency metrics.Histogram - service alarms.Service -} - -var _ alarms.Service = (*metricsMiddleware)(nil) - -func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service alarms.Service) alarms.Service { - return &metricsMiddleware{ - counter: counter, - latency: latency, - service: service, - } -} - -func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - defer func(begin time.Time) { - mm.counter.With("method", "create_alarm").Add(1) - mm.latency.With("method", "create_alarm").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.CreateAlarm(ctx, alarm) -} - -func (mm *metricsMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_alarm").Add(1) - mm.latency.With("method", "update_alarm").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateAlarm(ctx, session, alarm) -} - -func (mm *metricsMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) { - defer func(begin time.Time) { - mm.counter.With("method", "get_alarm").Add(1) - mm.latency.With("method", "get_alarm").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ViewAlarm(ctx, session, id) -} - -func (mm *metricsMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - defer func(begin time.Time) { - mm.counter.With("method", "list_alarms").Add(1) - mm.latency.With("method", "list_alarms").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ListAlarms(ctx, session, pm) -} - -func (mm *metricsMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error { - defer func(begin time.Time) { - mm.counter.With("method", "delete_alarm").Add(1) - mm.latency.With("method", "delete_alarm").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.DeleteAlarm(ctx, session, id) -} diff --git a/alarms/middleware/tracing.go b/alarms/middleware/tracing.go deleted file mode 100644 index a6b6a19d3..000000000 --- a/alarms/middleware/tracing.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/authn" - smqTracing "github.com/absmach/magistrala/pkg/tracing" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type tracingMiddleware struct { - tracer trace.Tracer - svc alarms.Service -} - -var _ alarms.Service = (*tracingMiddleware)(nil) - -func NewTracingMiddleware(tracer trace.Tracer, svc alarms.Service) alarms.Service { - return &tracingMiddleware{ - tracer: tracer, - svc: svc, - } -} - -func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "create_alarm", trace.WithAttributes( - attribute.String("rule_id", alarm.RuleID), - attribute.String("measurement", alarm.Measurement), - attribute.String("value", alarm.Value), - attribute.String("unit", alarm.Unit), - attribute.String("cause", alarm.Cause), - attribute.String("status", alarm.Status.String()), - )) - defer span.End() - - return tm.svc.CreateAlarm(ctx, alarm) -} - -func (tm *tracingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_alarm", trace.WithAttributes( - attribute.String("rule_id", alarm.RuleID), - attribute.String("measurement", alarm.Measurement), - attribute.String("value", alarm.Value), - attribute.String("unit", alarm.Unit), - attribute.String("cause", alarm.Cause), - attribute.String("status", alarm.Status.String()), - )) - defer span.End() - - return tm.svc.UpdateAlarm(ctx, session, alarm) -} - -func (tm *tracingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "get_alarm", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.ViewAlarm(ctx, session, id) -} - -func (tm *tracingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_alarms", trace.WithAttributes( - attribute.Int("offset", int(pm.Offset)), - attribute.Int("limit", int(pm.Limit)), - )) - defer span.End() - - return tm.svc.ListAlarms(ctx, session, pm) -} - -func (tm *tracingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "delete_alarm", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.DeleteAlarm(ctx, session, id) -} diff --git a/alarms/mocks/repository.go b/alarms/mocks/repository.go deleted file mode 100644 index f44c3c6ff..000000000 --- a/alarms/mocks/repository.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/alarms" - mock "github.com/stretchr/testify/mock" -) - -// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -type Repository_Expecter struct { - mock *mock.Mock -} - -func (_m *Repository) EXPECT() *Repository_Expecter { - return &Repository_Expecter{mock: &_m.Mock} -} - -// CreateAlarm provides a mock function for the type Repository -func (_mock *Repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - ret := _mock.Called(ctx, alarm) - - if len(ret) == 0 { - panic("no return value specified for CreateAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok { - return returnFunc(ctx, alarm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok { - r0 = returnFunc(ctx, alarm) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok { - r1 = returnFunc(ctx, alarm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm' -type Repository_CreateAlarm_Call struct { - *mock.Call -} - -// CreateAlarm is a helper method to define mock.On call -// - ctx context.Context -// - alarm alarms.Alarm -func (_e *Repository_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Repository_CreateAlarm_Call { - return &Repository_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)} -} - -func (_c *Repository_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_CreateAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 alarms.Alarm - if args[1] != nil { - arg1 = args[1].(alarms.Alarm) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_CreateAlarm_Call { - _c.Call.Return(alarm1, err) - return _c -} - -func (_c *Repository_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_CreateAlarm_Call { - _c.Call.Return(run) - return _c -} - -// DeleteAlarm provides a mock function for the type Repository -func (_mock *Repository) DeleteAlarm(ctx context.Context, id string) error { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteAlarm") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm' -type Repository_DeleteAlarm_Call struct { - *mock.Call -} - -// DeleteAlarm is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *Repository_Expecter) DeleteAlarm(ctx interface{}, id interface{}) *Repository_DeleteAlarm_Call { - return &Repository_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, id)} -} - -func (_c *Repository_DeleteAlarm_Call) Run(run func(ctx context.Context, id string)) *Repository_DeleteAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_DeleteAlarm_Call) Return(err error) *Repository_DeleteAlarm_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_DeleteAlarm_Call { - _c.Call.Return(run) - return _c -} - -// ListAllAlarms provides a mock function for the type Repository -func (_mock *Repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - ret := _mock.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for ListAllAlarms") - } - - var r0 alarms.AlarmsPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok { - return returnFunc(ctx, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) alarms.AlarmsPage); ok { - r0 = returnFunc(ctx, pm) - } else { - r0 = ret.Get(0).(alarms.AlarmsPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.PageMetadata) error); ok { - r1 = returnFunc(ctx, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ListAllAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllAlarms' -type Repository_ListAllAlarms_Call struct { - *mock.Call -} - -// ListAllAlarms is a helper method to define mock.On call -// - ctx context.Context -// - pm alarms.PageMetadata -func (_e *Repository_Expecter) ListAllAlarms(ctx interface{}, pm interface{}) *Repository_ListAllAlarms_Call { - return &Repository_ListAllAlarms_Call{Call: _e.mock.On("ListAllAlarms", ctx, pm)} -} - -func (_c *Repository_ListAllAlarms_Call) Run(run func(ctx context.Context, pm alarms.PageMetadata)) *Repository_ListAllAlarms_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 alarms.PageMetadata - if args[1] != nil { - arg1 = args[1].(alarms.PageMetadata) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_ListAllAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListAllAlarms_Call { - _c.Call.Return(alarmsPage, err) - return _c -} - -func (_c *Repository_ListAllAlarms_Call) RunAndReturn(run func(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListAllAlarms_Call { - _c.Call.Return(run) - return _c -} - -// UpdateAlarm provides a mock function for the type Repository -func (_mock *Repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - ret := _mock.Called(ctx, alarm) - - if len(ret) == 0 { - panic("no return value specified for UpdateAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok { - return returnFunc(ctx, alarm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok { - r0 = returnFunc(ctx, alarm) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok { - r1 = returnFunc(ctx, alarm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm' -type Repository_UpdateAlarm_Call struct { - *mock.Call -} - -// UpdateAlarm is a helper method to define mock.On call -// - ctx context.Context -// - alarm alarms.Alarm -func (_e *Repository_Expecter) UpdateAlarm(ctx interface{}, alarm interface{}) *Repository_UpdateAlarm_Call { - return &Repository_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, alarm)} -} - -func (_c *Repository_UpdateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_UpdateAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 alarms.Alarm - if args[1] != nil { - arg1 = args[1].(alarms.Alarm) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_UpdateAlarm_Call { - _c.Call.Return(alarm1, err) - return _c -} - -func (_c *Repository_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_UpdateAlarm_Call { - _c.Call.Return(run) - return _c -} - -// ViewAlarm provides a mock function for the type Repository -func (_mock *Repository) ViewAlarm(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error) { - ret := _mock.Called(ctx, alarmID, domainID) - - if len(ret) == 0 { - panic("no return value specified for ViewAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (alarms.Alarm, error)); ok { - return returnFunc(ctx, alarmID, domainID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) alarms.Alarm); ok { - r0 = returnFunc(ctx, alarmID, domainID) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = returnFunc(ctx, alarmID, domainID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm' -type Repository_ViewAlarm_Call struct { - *mock.Call -} - -// ViewAlarm is a helper method to define mock.On call -// - ctx context.Context -// - alarmID string -// - domainID string -func (_e *Repository_Expecter) ViewAlarm(ctx interface{}, alarmID interface{}, domainID interface{}) *Repository_ViewAlarm_Call { - return &Repository_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, alarmID, domainID)} -} - -func (_c *Repository_ViewAlarm_Call) Run(run func(ctx context.Context, alarmID string, domainID string)) *Repository_ViewAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Repository_ViewAlarm_Call { - _c.Call.Return(alarm, err) - return _c -} - -func (_c *Repository_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error)) *Repository_ViewAlarm_Call { - _c.Call.Return(run) - return _c -} diff --git a/alarms/mocks/service.go b/alarms/mocks/service.go deleted file mode 100644 index 0d6df07b4..000000000 --- a/alarms/mocks/service.go +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/authn" - mock "github.com/stretchr/testify/mock" -) - -// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewService(t interface { - mock.TestingT - Cleanup(func()) -}) *Service { - mock := &Service{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Service is an autogenerated mock type for the Service type -type Service struct { - mock.Mock -} - -type Service_Expecter struct { - mock *mock.Mock -} - -func (_m *Service) EXPECT() *Service_Expecter { - return &Service_Expecter{mock: &_m.Mock} -} - -// CreateAlarm provides a mock function for the type Service -func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - ret := _mock.Called(ctx, alarm) - - if len(ret) == 0 { - panic("no return value specified for CreateAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok { - return returnFunc(ctx, alarm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok { - r0 = returnFunc(ctx, alarm) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok { - r1 = returnFunc(ctx, alarm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm' -type Service_CreateAlarm_Call struct { - *mock.Call -} - -// CreateAlarm is a helper method to define mock.On call -// - ctx context.Context -// - alarm alarms.Alarm -func (_e *Service_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Service_CreateAlarm_Call { - return &Service_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)} -} - -func (_c *Service_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Service_CreateAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 alarms.Alarm - if args[1] != nil { - arg1 = args[1].(alarms.Alarm) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Service_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_CreateAlarm_Call { - _c.Call.Return(alarm1, err) - return _c -} - -func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_CreateAlarm_Call { - _c.Call.Return(run) - return _c -} - -// DeleteAlarm provides a mock function for the type Service -func (_mock *Service) DeleteAlarm(ctx context.Context, session authn.Session, id string) error { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteAlarm") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm' -type Service_DeleteAlarm_Call struct { - *mock.Call -} - -// DeleteAlarm is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) DeleteAlarm(ctx interface{}, session interface{}, id interface{}) *Service_DeleteAlarm_Call { - return &Service_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, session, id)} -} - -func (_c *Service_DeleteAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DeleteAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_DeleteAlarm_Call) Return(err error) *Service_DeleteAlarm_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_DeleteAlarm_Call { - _c.Call.Return(run) - return _c -} - -// ListAlarms provides a mock function for the type Service -func (_mock *Service) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - ret := _mock.Called(ctx, session, pm) - - if len(ret) == 0 { - panic("no return value specified for ListAlarms") - } - - var r0 alarms.AlarmsPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok { - return returnFunc(ctx, session, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) alarms.AlarmsPage); ok { - r0 = returnFunc(ctx, session, pm) - } else { - r0 = ret.Get(0).(alarms.AlarmsPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.PageMetadata) error); ok { - r1 = returnFunc(ctx, session, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ListAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAlarms' -type Service_ListAlarms_Call struct { - *mock.Call -} - -// ListAlarms is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - pm alarms.PageMetadata -func (_e *Service_Expecter) ListAlarms(ctx interface{}, session interface{}, pm interface{}) *Service_ListAlarms_Call { - return &Service_ListAlarms_Call{Call: _e.mock.On("ListAlarms", ctx, session, pm)} -} - -func (_c *Service_ListAlarms_Call) Run(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata)) *Service_ListAlarms_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 alarms.PageMetadata - if args[2] != nil { - arg2 = args[2].(alarms.PageMetadata) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_ListAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Service_ListAlarms_Call { - _c.Call.Return(alarmsPage, err) - return _c -} - -func (_c *Service_ListAlarms_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Service_ListAlarms_Call { - _c.Call.Return(run) - return _c -} - -// UpdateAlarm provides a mock function for the type Service -func (_mock *Service) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) { - ret := _mock.Called(ctx, session, alarm) - - if len(ret) == 0 { - panic("no return value specified for UpdateAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) (alarms.Alarm, error)); ok { - return returnFunc(ctx, session, alarm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) alarms.Alarm); ok { - r0 = returnFunc(ctx, session, alarm) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.Alarm) error); ok { - r1 = returnFunc(ctx, session, alarm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm' -type Service_UpdateAlarm_Call struct { - *mock.Call -} - -// UpdateAlarm is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - alarm alarms.Alarm -func (_e *Service_Expecter) UpdateAlarm(ctx interface{}, session interface{}, alarm interface{}) *Service_UpdateAlarm_Call { - return &Service_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, session, alarm)} -} - -func (_c *Service_UpdateAlarm_Call) Run(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm)) *Service_UpdateAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 alarms.Alarm - if args[2] != nil { - arg2 = args[2].(alarms.Alarm) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_UpdateAlarm_Call { - _c.Call.Return(alarm1, err) - return _c -} - -func (_c *Service_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_UpdateAlarm_Call { - _c.Call.Return(run) - return _c -} - -// ViewAlarm provides a mock function for the type Service -func (_mock *Service) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewAlarm") - } - - var r0 alarms.Alarm - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (alarms.Alarm, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) alarms.Alarm); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(alarms.Alarm) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm' -type Service_ViewAlarm_Call struct { - *mock.Call -} - -// ViewAlarm is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) ViewAlarm(ctx interface{}, session interface{}, id interface{}) *Service_ViewAlarm_Call { - return &Service_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, session, id)} -} - -func (_c *Service_ViewAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewAlarm_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Service_ViewAlarm_Call { - _c.Call.Return(alarm, err) - return _c -} - -func (_c *Service_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error)) *Service_ViewAlarm_Call { - _c.Call.Return(run) - return _c -} diff --git a/alarms/operations/operations.go b/alarms/operations/operations.go deleted file mode 100644 index 13c0a09ce..000000000 --- a/alarms/operations/operations.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package operations - -import "github.com/absmach/magistrala/pkg/permissions" - -const EntityType = "alarm" - -// Alarm Operations. -const ( - OpViewAlarm permissions.Operation = iota - OpDeleteAlarm - OpListAlarms - OpAssignAlarm - OpAcknowledgeAlarm - OpResolveAlarm - OpUpdateAlarm -) - -func OperationDetails() map[permissions.Operation]permissions.OperationDetails { - return map[permissions.Operation]permissions.OperationDetails{ - OpViewAlarm: { - Name: "view", - PermissionRequired: true, - }, - OpDeleteAlarm: { - Name: "delete", - PermissionRequired: true, - }, - OpListAlarms: { - Name: "list", - PermissionRequired: true, - }, - OpAssignAlarm: { - Name: "alarm_assign", - PermissionRequired: true, - }, - OpAcknowledgeAlarm: { - Name: "alarm_acknowledge", - PermissionRequired: true, - }, - OpResolveAlarm: { - Name: "alarm_resolve", - PermissionRequired: true, - }, - OpUpdateAlarm: { - Name: "update", - PermissionRequired: true, - }, - } -} diff --git a/alarms/postgres/alarms.go b/alarms/postgres/alarms.go deleted file mode 100644 index c5ebb72a4..000000000 --- a/alarms/postgres/alarms.go +++ /dev/null @@ -1,510 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "math" - "strings" - "time" - - "github.com/absmach/magistrala/alarms" - api "github.com/absmach/magistrala/api/http" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/jmoiron/sqlx" -) - -const alarmColumns = `alarms.id, alarms.rule_id, alarms.domain_id, alarms.channel_id, alarms.client_id, alarms.subtopic, alarms.measurement, alarms.value, alarms.unit, -alarms.threshold, alarms.cause, alarms.status, alarms.severity, alarms.assignee_id, alarms.created_at, alarms.updated_at, alarms.updated_by, alarms.assigned_at, -alarms.assigned_by, alarms.acknowledged_at, alarms.acknowledged_by, alarms.resolved_at, alarms.resolved_by, alarms.metadata` - -type repository struct { - db *sqlx.DB -} - -var _ alarms.Repository = (*repository)(nil) - -func NewAlarmsRepo(db *sqlx.DB) alarms.Repository { - return &repository{db: db} -} - -func (r *repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - query := ` - WITH existing AS ( - SELECT status, severity - FROM alarms - WHERE domain_id = :domain_id - AND rule_id = :rule_id - AND channel_id = :channel_id - AND client_id = :client_id - AND subtopic = :subtopic - AND measurement = :measurement - AND created_at <= :created_at - ORDER BY created_at DESC - LIMIT 1 - ) - INSERT INTO alarms ( - id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, - value, unit, threshold, cause, status, severity, assignee_id, - created_at, updated_at, updated_by, assigned_at, assigned_by, - acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata - ) - SELECT - :id, :rule_id, :domain_id, :channel_id, :client_id, :subtopic, :measurement, - :value, :unit, :threshold, :cause, :status, :severity, :assignee_id, - :created_at, :updated_at, :updated_by, :assigned_at, :assigned_by, - :acknowledged_at, :acknowledged_by, :resolved_at, :resolved_by, :metadata - WHERE ( - EXISTS ( - SELECT 1 FROM existing - WHERE existing.status IS DISTINCT FROM :status - OR (:status = 0 AND existing.status = 0 AND existing.severity IS DISTINCT FROM :severity) - ) - OR ( - NOT EXISTS (SELECT 1 FROM existing) AND :status = 0 - ) - ) - RETURNING - id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, - value, unit, threshold, cause, status, severity, created_at, - assignee_id, updated_at, updated_by, assigned_at, assigned_by, - acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata - ; - ` - dba, err := toDBAlarm(alarm) - if err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err) - } - row, err := r.db.NamedQueryContext(ctx, query, dba) - if err != nil { - return alarms.Alarm{}, postgres.HandleError(repoerr.ErrCreateEntity, err) - } - defer row.Close() - - if !row.Next() { - return alarms.Alarm{}, repoerr.ErrNotFound - } - - dba = dbAlarm{} - if err := row.StructScan(&dba); err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err) - } - - return toAlarm(dba) -} - -func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) { - var query []string - var upq string - if alarm.Status != 0 { - query = append(query, "status = :status,") - } - if alarm.AssigneeID != "" { - query = append(query, "assignee_id = :assignee_id,") - } - if !alarm.AssignedAt.IsZero() { - query = append(query, "assigned_at = :assigned_at,") - } - if alarm.AssignedBy != "" { - query = append(query, "assigned_by = :assigned_by,") - } - if alarm.AcknowledgedBy != "" { - query = append(query, "acknowledged_by = :acknowledged_by,") - } - if !alarm.AcknowledgedAt.IsZero() { - query = append(query, "acknowledged_at = :acknowledged_at,") - } - if alarm.ResolvedBy != "" { - query = append(query, "resolved_by = :resolved_by,") - } - if !alarm.ResolvedAt.IsZero() { - query = append(query, "resolved_at = :resolved_at,") - } - if alarm.Metadata != nil { - query = append(query, "metadata = :metadata,") - } - if len(query) > 0 { - upq = strings.Join(query, " ") - } - - q := fmt.Sprintf(`UPDATE alarms SET %s updated_by = :updated_by, updated_at = :updated_at WHERE id = :id - RETURNING id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, value, unit, threshold, - cause, status, severity, assignee_id, assigned_at, assigned_by, acknowledged_at, acknowledged_by, - resolved_by, resolved_at, metadata, created_at, updated_by, updated_at;`, upq) - - dba, err := toDBAlarm(alarm) - if err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - row, err := r.db.NamedQueryContext(ctx, q, dba) - if err != nil { - return alarms.Alarm{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - if !row.Next() { - return alarms.Alarm{}, repoerr.ErrNotFound - } - - dba = dbAlarm{} - if err := row.StructScan(&dba); err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - return toAlarm(dba) -} - -func (r *repository) ViewAlarm(ctx context.Context, alarmID, domainID string) (alarms.Alarm, error) { - query := `SELECT * FROM alarms WHERE id = :id AND domain_id = :domain_id;` - row, err := r.db.NamedQueryContext(ctx, query, map[string]any{ - "id": alarmID, "domain_id": domainID, - }) - if err != nil { - return alarms.Alarm{}, postgres.HandleError(repoerr.ErrViewEntity, err) - } - defer row.Close() - - if !row.Next() { - return alarms.Alarm{}, repoerr.ErrNotFound - } - - dba := dbAlarm{} - if err := row.StructScan(&dba); err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - alarm, err := toAlarm(dba) - if err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - return alarm, nil -} - -func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - query, err := pageQuery(pm) - if err != nil { - return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - comQuery := fmt.Sprintf(`SELECT %s FROM alarms %s`, alarmColumns, query) - - return r.alarmsPage(ctx, comQuery, pm) -} - -func (r *repository) alarmsPage(ctx context.Context, comQuery string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) { - dir := api.DescDir - if pm.Dir == api.AscDir { - dir = api.AscDir - } - - var orderClause string - switch pm.Order { - case api.CreatedAtOrder: - orderClause = fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir) - default: - orderClause = fmt.Sprintf("ORDER BY COALESCE(updated_at, created_at) %s, id %s", dir, dir) - } - - q := fmt.Sprintf(`SELECT * FROM (%s) AS sub_query %s LIMIT :limit OFFSET :offset;`, comQuery, orderClause) - cq := fmt.Sprintf(`SELECT COUNT(*) AS total_count FROM (%s) AS sub_query;`, comQuery) - - rows, err := r.db.NamedQueryContext(ctx, q, pm) - if err != nil { - return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - defer rows.Close() - - var items []alarms.Alarm - for rows.Next() { - dba := dbAlarm{} - if err := rows.StructScan(&dba); err != nil { - return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - a, err := toAlarm(dba) - if err != nil { - return alarms.AlarmsPage{}, err - } - - items = append(items, a) - } - - total, err := postgres.Total(ctx, r.db, cq, pm) - if err != nil { - return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - return alarms.AlarmsPage{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - Alarms: items, - }, nil -} - -func (r *repository) DeleteAlarm(ctx context.Context, id string) error { - query := `DELETE FROM alarms WHERE id = :id;` - result, err := r.db.NamedExecContext(ctx, query, map[string]any{"id": id}) - if err != nil { - return errors.Wrap(repoerr.ErrRemoveEntity, err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return errors.Wrap(repoerr.ErrRemoveEntity, err) - } - - if rowsAffected == 0 { - return repoerr.ErrNotFound - } - - return nil -} - -type dbAlarm struct { - ID string `db:"id"` - RuleID string `db:"rule_id"` - DomainID string `db:"domain_id"` - ChannelID string `db:"channel_id"` - ClientID string `db:"client_id"` - Subtopic string `db:"subtopic"` - Measurement string `db:"measurement"` - Value string `db:"value"` - Unit string `db:"unit"` - Cause string `db:"cause"` - Threshold string `db:"threshold"` - Status alarms.Status `db:"status"` - Severity uint8 `db:"severity"` - AssigneeID string `db:"assignee_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at,omitempty"` - UpdatedBy *string `db:"updated_by,omitempty"` - AssignedAt sql.NullTime `db:"assigned_at,omitempty"` - AssignedBy *string `db:"assigned_by,omitempty"` - AcknowledgedAt sql.NullTime `db:"acknowledged_at,omitempty"` - AcknowledgedBy *string `db:"acknowledged_by,omitempty"` - ResolvedAt sql.NullTime `db:"resolved_at,omitempty"` - ResolvedBy *string `db:"resolved_by,omitempty"` - Metadata []byte `db:"metadata,omitempty"` -} - -func toDBAlarm(a alarms.Alarm) (dbAlarm, error) { - if a.CreatedAt.IsZero() { - a.CreatedAt = time.Now() - } - var updatedBy *string - if a.UpdatedBy != "" { - updatedBy = &a.UpdatedBy - } - var updatedAt sql.NullTime - if a.UpdatedAt != (time.Time{}) { - updatedAt = sql.NullTime{Time: a.UpdatedAt, Valid: true} - } - - var acknowledgedBy *string - if a.AcknowledgedBy != "" { - acknowledgedBy = &a.AcknowledgedBy - } - var acknowledgedAt sql.NullTime - if a.AcknowledgedAt != (time.Time{}) { - acknowledgedAt = sql.NullTime{Time: a.AcknowledgedAt, Valid: true} - } - - var resolvedBy *string - if a.ResolvedBy != "" { - resolvedBy = &a.ResolvedBy - } - var resolvedAt sql.NullTime - if a.ResolvedAt != (time.Time{}) { - resolvedAt = sql.NullTime{Time: a.ResolvedAt, Valid: true} - } - - var assignedBy *string - if a.AssignedBy != "" { - assignedBy = &a.AssignedBy - } - var assignedAt sql.NullTime - if a.AssignedAt != (time.Time{}) { - assignedAt = sql.NullTime{Time: a.AssignedAt, Valid: true} - } - - metadata := []byte("{}") - if len(a.Metadata) > 0 { - b, err := json.Marshal(a.Metadata) - if err != nil { - return dbAlarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - metadata = b - } - - return dbAlarm{ - ID: a.ID, - RuleID: a.RuleID, - DomainID: a.DomainID, - ChannelID: a.ChannelID, - ClientID: a.ClientID, - Subtopic: a.Subtopic, - Measurement: a.Measurement, - Value: a.Value, - Unit: a.Unit, - Cause: a.Cause, - Threshold: a.Threshold, - Status: a.Status, - Severity: a.Severity, - AssigneeID: a.AssigneeID, - CreatedAt: a.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - AssignedAt: assignedAt, - AssignedBy: assignedBy, - AcknowledgedAt: acknowledgedAt, - AcknowledgedBy: acknowledgedBy, - ResolvedAt: resolvedAt, - ResolvedBy: resolvedBy, - Metadata: metadata, - }, nil -} - -func toAlarm(dbr dbAlarm) (alarms.Alarm, error) { - var updatedBy string - if dbr.UpdatedBy != nil { - updatedBy = *dbr.UpdatedBy - } - var updatedAt time.Time - if dbr.UpdatedAt.Valid { - updatedAt = dbr.UpdatedAt.Time - } - - var assignedBy string - if dbr.AssignedBy != nil { - assignedBy = *dbr.AssignedBy - } - var assignedAt time.Time - if dbr.AssignedAt.Valid { - assignedAt = dbr.AssignedAt.Time - } - - var acknowledgedBy string - if dbr.AcknowledgedBy != nil { - acknowledgedBy = *dbr.AcknowledgedBy - } - var acknowledgedAt time.Time - if dbr.AcknowledgedAt.Valid { - acknowledgedAt = dbr.AcknowledgedAt.Time - } - - var resolvedBy string - if dbr.ResolvedBy != nil { - resolvedBy = *dbr.ResolvedBy - } - var resolvedAt time.Time - if dbr.ResolvedAt.Valid { - resolvedAt = dbr.ResolvedAt.Time - } - - var metadata map[string]any - if len(dbr.Metadata) > 0 { - err := json.Unmarshal(dbr.Metadata, &metadata) - if err != nil { - return alarms.Alarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - } - - return alarms.Alarm{ - ID: dbr.ID, - RuleID: dbr.RuleID, - DomainID: dbr.DomainID, - ChannelID: dbr.ChannelID, - ClientID: dbr.ClientID, - Subtopic: dbr.Subtopic, - Measurement: dbr.Measurement, - Value: dbr.Value, - Unit: dbr.Unit, - Threshold: dbr.Threshold, - Cause: dbr.Cause, - Status: dbr.Status, - Severity: dbr.Severity, - AssigneeID: dbr.AssigneeID, - CreatedAt: dbr.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - AssignedAt: assignedAt, - AssignedBy: assignedBy, - AcknowledgedAt: acknowledgedAt, - AcknowledgedBy: acknowledgedBy, - ResolvedAt: resolvedAt, - ResolvedBy: resolvedBy, - Metadata: metadata, - }, nil -} - -func pageQuery(pm alarms.PageMetadata) (string, error) { - query := pageQueryConditions(pm) - - var emq string - if len(query) > 0 { - emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - - return emq, nil -} - -func pageQueryConditions(pm alarms.PageMetadata) []string { - var query []string - if pm.DomainID != "" { - query = append(query, "alarms.domain_id = :domain_id") - } - if pm.RuleID != "" { - query = append(query, "alarms.rule_id = :rule_id") - } - if len(pm.RuleIDs) > 0 { - query = append(query, "alarms.rule_id = ANY(:rule_ids)") - } - if pm.ChannelID != "" { - query = append(query, "alarms.channel_id = :channel_id") - } - if pm.Subtopic != "" { - query = append(query, "alarms.subtopic = :subtopic") - } - if pm.ClientID != "" { - query = append(query, "alarms.client_id = :client_id") - } - if pm.Measurement != "" { - query = append(query, "alarms.measurement = :measurement") - } - if pm.Status != alarms.AllStatus { - query = append(query, "alarms.status = :status") - } - if pm.Severity != math.MaxUint8 { - query = append(query, "alarms.severity = :severity") - } - if pm.AssigneeID != "" { - query = append(query, "alarms.assignee_id = :assignee_id") - } - if pm.UpdatedBy != "" { - query = append(query, "alarms.updated_by = :updated_by") - } - if pm.ResolvedBy != "" { - query = append(query, "alarms.resolved_by = :resolved_by") - } - if pm.AcknowledgedBy != "" { - query = append(query, "alarms.acknowledged_by = :acknowledged_by") - } - if pm.AssignedBy != "" { - query = append(query, "alarms.assigned_by = :assigned_by") - } - if !pm.CreatedFrom.IsZero() { - query = append(query, "alarms.created_at >= :created_from") - } - if !pm.CreatedTo.IsZero() { - query = append(query, "alarms.created_at <= :created_to") - } - return query -} diff --git a/alarms/postgres/alarms_test.go b/alarms/postgres/alarms_test.go deleted file mode 100644 index d663fc7df..000000000 --- a/alarms/postgres/alarms_test.go +++ /dev/null @@ -1,481 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/alarms/postgres" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - namegen = namegenerator.NewGenerator() - idProvider = uuid.New() -) - -func TestCreateAlarm(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM alarms") - require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err)) - }) - - repo := postgres.NewAlarmsRepo(db) - - alarm := alarms.Alarm{ - ID: generateUUID(t), - RuleID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Subtopic: namegen.Generate(), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - } - - cases := []struct { - desc string - alarm alarms.Alarm - err error - }{ - { - desc: "valid alarm", - alarm: alarm, - err: nil, - }, - { - desc: "duplicate alarm", - alarm: alarm, - err: repoerr.ErrNotFound, - }, - { - desc: "missing rule id", - alarm: alarms.Alarm{ - ID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Subtopic: namegen.Generate(), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - - Metadata: map[string]any{ - "key": "value", - }, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "invalid alarm", - alarm: alarms.Alarm{ - ID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Subtopic: namegen.Generate(), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - - Metadata: map[string]any{ - "key": make(chan int), - }, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "empty alarm", - alarm: alarms.Alarm{}, - err: repoerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - alarm, err := repo.CreateAlarm(context.Background(), tc.alarm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - assert.NotEmpty(t, alarm.ID) - assert.Equal(t, tc.alarm.RuleID, alarm.RuleID) - assert.Equal(t, tc.alarm.Measurement, alarm.Measurement) - assert.Equal(t, tc.alarm.Value, alarm.Value) - assert.Equal(t, tc.alarm.Unit, alarm.Unit) - assert.Equal(t, tc.alarm.Cause, alarm.Cause) - assert.Equal(t, tc.alarm.Status, alarm.Status) - assert.Equal(t, tc.alarm.DomainID, alarm.DomainID) - assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID) - assert.Equal(t, tc.alarm.Metadata, alarm.Metadata) - }) - } -} - -func TestUpdateAlarm(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM alarms") - require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err)) - }) - - repo := postgres.NewAlarmsRepo(db) - - alarm := alarms.Alarm{ - ID: generateUUID(t), - RuleID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - } - alarm, err := repo.CreateAlarm(context.Background(), alarm) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - alarm alarms.Alarm - err error - }{ - { - desc: "valid alarm", - alarm: alarms.Alarm{ - ID: alarm.ID, - Status: alarms.ClearedStatus, - DomainID: alarm.DomainID, - AssigneeID: generateUUID(t), - AssignedBy: generateUUID(t), - AssignedAt: time.Now().UTC(), - AcknowledgedBy: generateUUID(t), - AcknowledgedAt: time.Now().UTC(), - CreatedAt: alarm.CreatedAt, - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - ResolvedAt: time.Now().UTC(), - ResolvedBy: generateUUID(t), - Metadata: map[string]any{ - "key": "value", - }, - }, - err: nil, - }, - { - desc: "non existing alarm", - alarm: alarms.Alarm{ - ID: generateUUID(t), - }, - err: repoerr.ErrNotFound, - }, - { - desc: "invalid alarm", - alarm: alarms.Alarm{ - ID: alarm.ID, - RuleID: generateUUID(t), - Status: 0, - DomainID: generateUUID(t), - AssigneeID: strings.Repeat("a", 40), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - }, - err: repoerr.ErrMalformedEntity, - }, - { - desc: "empty alarm", - alarm: alarms.Alarm{}, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - alarm, err := repo.UpdateAlarm(context.Background(), tc.alarm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - assert.NotEmpty(t, alarm.ID) - assert.Equal(t, tc.alarm.Status, alarm.Status) - assert.Equal(t, tc.alarm.DomainID, alarm.DomainID) - assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID) - assert.Equal(t, tc.alarm.UpdatedBy, alarm.UpdatedBy) - assert.Equal(t, tc.alarm.ResolvedBy, alarm.ResolvedBy) - assert.Equal(t, tc.alarm.AcknowledgedBy, alarm.AcknowledgedBy) - assert.Equal(t, tc.alarm.Metadata, alarm.Metadata) - }) - } -} - -func TestViewAlarm(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM alarms") - require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err)) - }) - - repo := postgres.NewAlarmsRepo(db) - - alarm := alarms.Alarm{ - ID: generateUUID(t), - RuleID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - } - alarm, err := repo.CreateAlarm(context.Background(), alarm) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - domainID string - err error - }{ - { - desc: "valid alarm", - id: alarm.ID, - domainID: alarm.DomainID, - err: nil, - }, - { - desc: "non existing alarm id", - id: generateUUID(t), - domainID: alarm.DomainID, - err: repoerr.ErrNotFound, - }, - { - desc: "non existing domain id", - id: alarm.ID, - domainID: generateUUID(t), - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - alarm, err := repo.ViewAlarm(context.Background(), tc.id, tc.domainID) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - assert.NotEmpty(t, alarm.ID) - assert.Equal(t, tc.id, alarm.ID) - }) - } -} - -func TestListAlarms(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM alarms") - require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err)) - }) - repo := postgres.NewAlarmsRepo(db) - items := make([]alarms.Alarm, 1000) - for i := range 1000 { - items[i] = alarms.Alarm{ - ID: generateUUID(t), - RuleID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - } - alarm, err := repo.CreateAlarm(context.Background(), items[i]) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - items[i].ID = alarm.ID - } - - cases := []struct { - desc string - pm alarms.PageMetadata - response []alarms.Alarm - err error - }{ - { - desc: "valid page", - pm: alarms.PageMetadata{ - Offset: 0, - Limit: 10, - }, - response: items[:10], - err: nil, - }, - { - desc: "offset and limit", - pm: alarms.PageMetadata{ - Offset: 10, - Limit: 50, - }, - response: items[10:60], - err: nil, - }, - { - desc: "empty page", - pm: alarms.PageMetadata{}, - response: []alarms.Alarm{}, - err: nil, - }, - { - desc: "invalid page", - pm: alarms.PageMetadata{ - Offset: 1000, - Limit: 10, - }, - response: []alarms.Alarm{}, - err: nil, - }, - { - desc: "invalid assignee id", - pm: alarms.PageMetadata{ - Offset: 0, - Limit: 10, - AssigneeID: generateUUID(t), - }, - response: []alarms.Alarm{}, - err: nil, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - alarms, err := repo.ListAllAlarms(context.Background(), tc.pm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - assert.Equal(t, len(tc.response), len(alarms.Alarms)) - }) - } -} - -func TestDeleteAlarm(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM alarms") - require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err)) - }) - - repo := postgres.NewAlarmsRepo(db) - - alarm := alarms.Alarm{ - ID: generateUUID(t), - RuleID: generateUUID(t), - DomainID: generateUUID(t), - ChannelID: generateUUID(t), - ClientID: generateUUID(t), - Measurement: namegen.Generate(), - Value: namegen.Generate(), - Unit: namegen.Generate(), - Threshold: namegen.Generate(), - Cause: namegen.Generate(), - Status: 0, - AssigneeID: generateUUID(t), - CreatedAt: time.Now().UTC(), - Metadata: map[string]any{ - "key": "value", - }, - } - alarm, err := repo.CreateAlarm(context.Background(), alarm) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "valid alarm", - id: alarm.ID, - err: nil, - }, - { - desc: "non existing alarm", - id: generateUUID(t), - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.DeleteAlarm(context.Background(), tc.id) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - }) - } -} - -func generateUUID(t *testing.T) string { - ulid, err := idProvider.ID() - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - return ulid -} diff --git a/alarms/postgres/init.go b/alarms/postgres/init.go deleted file mode 100644 index 93f217531..000000000 --- a/alarms/postgres/init.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - migrate "github.com/rubenv/sql-migrate" -) - -// Migration of Alarms service. -func Migration() (*migrate.MemoryMigrationSource, error) { - alarmsMigration := &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "alarms_01", - // VARCHAR(36) for columns with IDs as UUIDS have a maximum of 36 characters - Up: []string{ - `CREATE TABLE IF NOT EXISTS alarms ( - id VARCHAR(36) PRIMARY KEY, - rule_id VARCHAR(36) NOT NULL CHECK (length(rule_id) > 0), - domain_id VARCHAR(36) NOT NULL, - channel_id VARCHAR(36) NOT NULL, - subtopic TEXT NOT NULL, - client_id VARCHAR(36) NOT NULL, - measurement TEXT NOT NULL, - value TEXT NOT NULL, - unit TEXT NOT NULL, - threshold TEXT NOT NULL, - cause TEXT NOT NULL, - status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), - severity SMALLINT NOT NULL DEFAULT 0 CHECK (severity >= 0), - assignee_id VARCHAR(36), - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NULL, - updated_by VARCHAR(36) NULL, - assigned_at TIMESTAMPTZ NULL, - assigned_by VARCHAR(36) NULL, - acknowledged_at TIMESTAMPTZ NULL, - acknowledged_by VARCHAR(36) NULL, - resolved_at TIMESTAMPTZ NULL, - resolved_by VARCHAR(36) NULL, - metadata JSONB - );`, - "CREATE INDEX IF NOT EXISTS idx_alarms_state ON alarms (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC);", - }, - Down: []string{ - `DROP TABLE IF EXISTS alarms`, - }, - }, - }, - } - - return alarmsMigration, nil -} diff --git a/alarms/postgres/setup_test.go b/alarms/postgres/setup_test.go deleted file mode 100644 index 73725a624..000000000 --- a/alarms/postgres/setup_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "database/sql" - "fmt" - "log" - "os" - "testing" - "time" - - apostgres "github.com/absmach/magistrala/alarms/postgres" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/jmoiron/sqlx" - dockertest "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "go.opentelemetry.io/otel" -) - -var ( - db *sqlx.DB - database postgres.Database - tracer = otel.Tracer("repo_tests") -) - -func TestMain(m *testing.M) { - pool, err := dockertest.NewPool("") - if err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - container, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "postgres", - Tag: "16.2-alpine", - Env: []string{ - "POSTGRES_USER=test", - "POSTGRES_PASSWORD=test", - "POSTGRES_DB=test", - "listen_addresses = '*'", - }, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - if err != nil { - log.Fatalf("Could not start container: %s", err) - } - - port := container.GetPort("5432/tcp") - - // exponential backoff-retry, because the application in the container might not be ready to accept connections yet - pool.MaxWait = 120 * time.Second - if err := pool.Retry(func() error { - url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port) - db, err := sql.Open("pgx", url) - if err != nil { - return err - } - return db.Ping() - }); err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - dbConfig := postgres.Config{ - Host: "localhost", - Port: port, - User: "test", - Pass: "test", - Name: "test", - SSLMode: "disable", - SSLCert: "", - SSLKey: "", - SSLRootCert: "", - } - - migration, err := apostgres.Migration() - if err != nil { - log.Fatalf("Could not get migration: %s", err) - } - if db, err = postgres.Setup(dbConfig, *migration); err != nil { - log.Fatalf("Could not setup test DB connection: %s", err) - } - - database = postgres.NewDatabase(db, dbConfig, tracer) - - code := m.Run() - - // Defers will not be run when using os.Exit - db.Close() - if err := pool.Purge(container); err != nil { - log.Fatalf("Could not purge container: %s", err) - } - - os.Exit(code) -} diff --git a/alarms/service.go b/alarms/service.go deleted file mode 100644 index 9fd8c41d7..000000000 --- a/alarms/service.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package alarms - -import ( - "context" - "time" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/pkg/authn" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" -) - -type service struct { - idp magistrala.IDProvider - repo Repository -} - -var _ Service = (*service)(nil) - -func NewService(idp magistrala.IDProvider, repo Repository) Service { - return &service{ - idp: idp, - repo: repo, - } -} - -func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) { - id, err := s.idp.ID() - if err != nil { - return Alarm{}, err - } - alarm.ID = id - if alarm.CreatedAt.IsZero() { - alarm.CreatedAt = time.Now() - } - - if err := alarm.Validate(); err != nil { - return Alarm{}, err - } - - created, err := s.repo.CreateAlarm(ctx, alarm) - if err != nil && err != repoerr.ErrNotFound { - return Alarm{}, err - } - if err == repoerr.ErrNotFound { - return Alarm{}, nil - } - - return created, nil -} - -func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID string) (Alarm, error) { - return s.repo.ViewAlarm(ctx, alarmID, session.DomainID) -} - -func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) { - pm.DomainID = session.DomainID - return s.repo.ListAllAlarms(ctx, pm) -} - -func (s *service) DeleteAlarm(ctx context.Context, session authn.Session, alarmID string) error { - return s.repo.DeleteAlarm(ctx, alarmID) -} - -func (s *service) UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error) { - alarm.UpdatedAt = time.Now() - alarm.UpdatedBy = session.UserID - - return s.repo.UpdateAlarm(ctx, alarm) -} diff --git a/alarms/service_test.go b/alarms/service_test.go deleted file mode 100644 index f10a735fe..000000000 --- a/alarms/service_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package alarms_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/alarms/mocks" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var idp = uuid.New() - -func newService(t *testing.T, repo *mocks.Repository) alarms.Service { - return alarms.NewService(idp, repo) -} - -func TestCreateAlarm(t *testing.T) { - repo := new(mocks.Repository) - svc := newService(t, repo) - ts := time.Now() - cases := []struct { - desc string - alarm alarms.Alarm - err error - }{ - { - desc: "valid alarm", - alarm: alarms.Alarm{ - RuleID: "rule-id", - DomainID: "domain-id", - ChannelID: "channel-id", - ClientID: "client-id", - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - CreatedAt: ts, - }, - err: nil, - }, - { - desc: "missing rule_id", - alarm: alarms.Alarm{ - DomainID: "domain-id", - ChannelID: "channel-id", - ClientID: "client-id", - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - CreatedAt: ts, - }, - err: errors.New("rule_id is required"), - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("CreateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err) - _, err := svc.CreateAlarm(context.Background(), tc.alarm) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - }) - } -} - -func TestViewAlarm(t *testing.T) { - repo := new(mocks.Repository) - svc := newService(t, repo) - - cases := []struct { - desc string - id string - domainID string - err error - }{ - { - desc: "valid alarm", - id: "alarm-id", - domainID: "domain-id", - err: nil, - }, - { - desc: "non existing alarm id", - id: "alarm-id", - domainID: "domain-id", - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - s := authn.Session{DomainID: tc.domainID} - repoCall := repo.On("ViewAlarm", context.Background(), tc.id, tc.domainID).Return(alarms.Alarm{}, tc.err) - _, err := svc.ViewAlarm(context.Background(), s, tc.id) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - repoCall.Unset() - }) - } -} - -func TestUpdateAlarm(t *testing.T) { - repo := new(mocks.Repository) - svc := newService(t, repo) - - cases := []struct { - desc string - alarm alarms.Alarm - err error - }{ - { - desc: "valid alarm", - alarm: alarms.Alarm{ - RuleID: "rule-id", - DomainID: "domain-id", - ChannelID: "channel-id", - ClientID: "client-id", - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: nil, - }, - { - desc: "non existing alarm", - alarm: alarms.Alarm{ - RuleID: "rule-id", - DomainID: "domain-id", - ChannelID: "channel-id", - ClientID: "client-id", - Subtopic: "subtopic", - Measurement: "measurement", - Value: "value", - Unit: "unit", - Cause: "cause", - Severity: 100, - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - s := authn.Session{DomainID: tc.alarm.DomainID} - repoCall := repo.On("UpdateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err) - _, err := svc.UpdateAlarm(context.Background(), s, tc.alarm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - repoCall.Unset() - }) - } -} - -func TestListAlarms(t *testing.T) { - repo := new(mocks.Repository) - svc := newService(t, repo) - - cases := []struct { - desc string - pm alarms.PageMetadata - page alarms.AlarmsPage - err error - }{ - { - desc: "valid page", - pm: alarms.PageMetadata{ - Offset: 0, - Limit: 10, - }, - page: alarms.AlarmsPage{ - Offset: 0, - Limit: 10, - Total: 10, - Alarms: []alarms.Alarm{}, - }, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - s := authn.Session{DomainID: tc.pm.DomainID} - repoCall := repo.On("ListAllAlarms", context.Background(), tc.pm).Return(tc.page, tc.err) - _, err := svc.ListAlarms(context.Background(), s, tc.pm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - repoCall.Unset() - }) - } -} - -func TestDeleteAlarm(t *testing.T) { - repo := new(mocks.Repository) - svc := newService(t, repo) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "valid alarm", - id: "alarm-id", - err: nil, - }, - { - desc: "non existing alarm", - id: "alarm-id", - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - s := authn.Session{DomainID: tc.id} - repoCall := repo.On("DeleteAlarm", context.Background(), tc.id).Return(tc.err) - err := svc.DeleteAlarm(context.Background(), s, tc.id) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - - return - } - repoCall.Unset() - }) - } -} diff --git a/alarms/status.go b/alarms/status.go deleted file mode 100644 index b81d1e1f8..000000000 --- a/alarms/status.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package alarms - -import ( - "encoding/json" - "strings" - - svcerr "github.com/absmach/magistrala/pkg/errors/service" -) - -type Status uint8 - -const ( - ActiveStatus Status = iota - ClearedStatus - - // AllStatus is used for querying purposes to list alarms irrespective - // of their status. It is never stored in the database as the actual - // Alarm status and should always be the largest value in this enumeration. - AllStatus -) - -const ( - Active = "active" - Cleared = "cleared" - Unknown = "unknown" - All = "all" -) - -// String converts alarm status to string literal. -func (s Status) String() string { - switch s { - case ActiveStatus: - return Active - case ClearedStatus: - return Cleared - default: - return Unknown - } -} - -// ToStatus converts string value to a valid Alarm status. -func ToStatus(status string) (Status, error) { - switch strings.ToLower(status) { - case Active: - return ActiveStatus, nil - case Cleared: - return ClearedStatus, nil - case All: - return AllStatus, nil - default: - return Status(0), svcerr.ErrInvalidStatus - } -} - -// Custom Marshaller for Alarm. -func (s Status) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -// Custom Unmarshaler for Alarm. -func (s *Status) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToStatus(str) - *s = val - - return err -} diff --git a/api/http/common_test.go b/api/http/common_test.go index 854e80fe6..43ea26ba7 100644 --- a/api/http/common_test.go +++ b/api/http/common_test.go @@ -13,9 +13,9 @@ import ( "github.com/absmach/magistrala" api "github.com/absmach/magistrala/api/http" apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/stretchr/testify/assert" ) diff --git a/apidocs/openapi/journal.yaml b/apidocs/openapi/journal.yaml deleted file mode 100644 index 00798fe24..000000000 --- a/apidocs/openapi/journal.yaml +++ /dev/null @@ -1,426 +0,0 @@ -# Copyright (c) Abstract Machines -# SPDX-License-Identifier: Apache-2.0 - -openapi: 3.0.3 -info: - title: Magistrala Journal Log Service - description: | - This is the Journal Log Server based on the OpenAPI 3.0 specification. It is the HTTP API for viewing journal log history. You can now help us improve the API whether it's by making changes to the definition itself or to the code. - Some useful links: - - [The Magistrala repository](https://github.com/absmach/magistrala) - contact: - email: info@mainflux.com - license: - name: Apache 2.0 - url: https://github.com/absmach/magistrala/blob/main/LICENSE - version: 0.18.0 - -servers: - - url: http://localhost:9021 - - url: https://localhost:9021 - -tags: - - name: journal-log - description: Everything about your Journal Log - externalDocs: - description: Find out more about Journal Log - url: http://docs.mainflux.io/ - -paths: - /journal/user/{userID}: - get: - tags: - - journal-log - summary: List user journal log - description: | - Retrieves a list of journal. Due to performance concerns, data - is retrieved in subsets. The API must ensure that the entire - dataset is consumed either by making subsequent requests, or by - increasing the subset size of the initial request. - parameters: - - $ref: "#/components/parameters/user_id" - - $ref: "#/components/parameters/offset" - - $ref: "#/components/parameters/limit" - - $ref: "#/components/parameters/operation" - - $ref: "#/components/parameters/with_attributes" - - $ref: "#/components/parameters/with_metadata" - - $ref: "#/components/parameters/from" - - $ref: "#/components/parameters/to" - - $ref: "#/components/parameters/dir" - security: - - bearerAuth: [] - responses: - "200": - $ref: "#/components/responses/JournalsPageRes" - "400": - description: Failed due to malformed query parameters. - "401": - description: Missing or invalid access token provided. - "403": - description: Failed to perform authorization over the entity. - "404": - description: A non-existent entity request. - "422": - description: Database can't process request. - "500": - $ref: "#/components/responses/ServiceError" - - /{domainID}/journal/client/{clientID}/telemetry: - get: - tags: - - journal-log - summary: View client telemetry - description: | - Retrieves telemetry data for a specific client within a domain. - This includes connection status, messages sent/received, and other metrics. - parameters: - - $ref: "#/components/parameters/domain_id" - - $ref: "#/components/parameters/client_id" - security: - - bearerAuth: [] - responses: - "200": - description: Client telemetry data retrieved successfully - content: - application/json: - schema: - $ref: "#/components/schemas/Telemetry" - "400": - description: Failed due to malformed request parameters. - "401": - description: Missing or invalid access token provided. - "403": - description: Failed to perform authorization over the entity. - "404": - description: Client not found. - "422": - description: Database can't process request. - "500": - $ref: "#/components/responses/ServiceError" - - /{domainID}/journal/{entityType}/{id}: - get: - tags: - - journal-log - summary: List entity journal log - description: | - Retrieves a list of journal. Due to performance concerns, data - is retrieved in subsets. The API must ensure that the entire - dataset is consumed either by making subsequent requests, or by - increasing the subset size of the initial request. - parameters: - - $ref: "#/components/parameters/domain_id" - - $ref: "#/components/parameters/entity_type" - - $ref: "#/components/parameters/id" - - $ref: "#/components/parameters/offset" - - $ref: "#/components/parameters/limit" - - $ref: "#/components/parameters/operation" - - $ref: "#/components/parameters/with_attributes" - - $ref: "#/components/parameters/with_metadata" - - $ref: "#/components/parameters/from" - - $ref: "#/components/parameters/to" - - $ref: "#/components/parameters/dir" - security: - - bearerAuth: [] - responses: - "200": - $ref: "#/components/responses/JournalsPageRes" - "400": - description: Failed due to malformed query parameters. - "401": - description: Missing or invalid access token provided. - "403": - description: Failed to perform authorization over the entity. - "404": - description: A non-existent entity request. - "422": - description: Database can't process request. - "500": - $ref: "#/components/responses/ServiceError" - - /health: - get: - summary: Retrieves service health check info. - tags: - - health - security: [] - responses: - "200": - $ref: "#/components/responses/HealthRes" - "500": - $ref: "#/components/responses/ServiceError" - -components: - schemas: - Telemetry: - type: object - properties: - client_id: - type: string - format: uuid - description: Unique identifier of the client - example: "bb7edb32-2eac-4aad-aebe-ed96fe073879" - domain_id: - type: string - format: uuid - description: Unique identifier of the domain - example: "29d425c8-542b-4614-8a4d-a5951945d720" - subscriptions: - type: integer - format: int64 - description: Number of active subscriptions for the client - example: 5 - inbound_messages: - type: integer - format: int64 - description: Number of messages received by the client - example: 1234567 - outbound_messages: - type: integer - format: int64 - description: Number of messages sent by the client - example: 987654 - first_seen: - type: string - format: date-time - description: Timestamp when the client was first seen - example: "2024-01-11T10:00:00.000Z" - last_seen: - type: string - format: date-time - description: Timestamp when the client was last seen - example: "2024-01-11T12:05:07.449053Z" - - Journal: - type: object - properties: - operation: - type: string - example: user.create - description: Journal operation. - occurred_at: - type: string - format: date-time - example: "2024-01-11T12:05:07.449053Z" - description: Time when the journal occurred. - attributes: - type: object - description: Journal attributes. - example: - { - "created_at": "2024-06-12T11:34:32.991591Z", - "id": "29d425c8-542b-4614-8a4d-a5951945d720", - "identity": "Gawne-Havlicek@email.com", - "name": "Newgard-Frisina", - "status": "enabled", - "updated_at": "2024-06-12T11:34:33.116795Z", - "updated_by": "ad228f20-4741-47c5-bef7-d871b541c019", - } - metadata: - type: object - description: Journal payload. - example: { "Update": "Calvo-Felkins" } - xml: - name: journal - - JournalPage: - type: object - properties: - journals: - type: array - minItems: 0 - uniqueItems: true - items: - $ref: "#/components/schemas/Journal" - total: - type: integer - example: 1 - description: Total number of items. - offset: - type: integer - description: Number of items to skip during retrieval. - limit: - type: integer - example: 10 - description: Maximum number of items to return in one page. - required: - - journals - - total - - offset - - Error: - type: object - properties: - error: - type: string - description: Error message - example: { "error": "malformed entity specification" } - - parameters: - domain_id: - name: domainID - description: Unique identifier for a domain. - in: path - schema: - type: string - format: uuid - required: true - example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - - client_id: - name: clientID - description: Unique identifier for a client - in: path - schema: - type: string - format: uuid - required: true - example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - - entity_type: - name: entityType - description: Type of entity, e.g. group, client, channel. - in: path - schema: - type: string - enum: - - group - - client - - channel - required: true - example: group - - user_id: - name: userID - description: Unique identifier for a user. - in: path - schema: - type: string - format: uuid - required: true - example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - - id: - name: id - description: Unique identifier for an entity, e.g. group, channel or client. Used together with entity_type. - in: path - schema: - type: string - format: uuid - required: true - example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - - offset: - name: offset - description: Number of items to skip during retrieval. - in: query - schema: - type: integer - default: 0 - minimum: 0 - required: false - example: 0 - - limit: - name: limit - description: Size of the subset to retrieve. - in: query - schema: - type: integer - default: 10 - maximum: 100 - minimum: 1 - required: false - example: 10 - - operation: - name: operation - description: Journal operation. - in: query - schema: - type: string - required: false - example: user.create - - with_attributes: - name: with_attributes - description: Include journal attributes. - in: query - schema: - type: boolean - required: false - example: true - - with_metadata: - name: with_metadata - description: Include journal metadata. - in: query - schema: - type: boolean - required: false - example: true - - from: - name: from - description: Start date in unix time. - in: query - schema: - type: integer - format: int64 - required: false - example: 1966777289 - - to: - name: to - description: End date in unix time. - in: query - schema: - type: integer - format: int64 - required: false - example: 1966777289 - - dir: - name: dir - description: Sort direction. - in: query - schema: - type: string - enum: - - asc - - desc - required: false - example: desc - - responses: - JournalsPageRes: - description: Data retrieved. - content: - application/json: - schema: - $ref: "#/components/schemas/JournalPage" - - HealthRes: - description: Service Health Check. - content: - application/health+json: - schema: - $ref: "./schemas/health_info.yaml" - - ServiceError: - description: Unexpected server-side error occurred. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: | - * User access: "Authorization: Bearer " - -security: - - bearerAuth: [] diff --git a/auth/cache/tokens_test.go b/auth/cache/tokens_test.go index 7e94708e8..a363f8da5 100644 --- a/auth/cache/tokens_test.go +++ b/auth/cache/tokens_test.go @@ -12,8 +12,8 @@ import ( "github.com/absmach/magistrala/auth" "github.com/absmach/magistrala/auth/cache" - "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/errors" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" ) diff --git a/auth/service_test.go b/auth/service_test.go index ea654a27b..57aae5d51 100644 --- a/auth/service_test.go +++ b/auth/service_test.go @@ -13,12 +13,12 @@ import ( "github.com/absmach/magistrala/auth" "github.com/absmach/magistrala/auth/mocks" - "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/policies" policymocks "github.com/absmach/magistrala/pkg/policies/mocks" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/absmach/magistrala/pkg/uuid" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" diff --git a/cli/config.go b/cli/config.go index 83b794ef0..7f4df3c3e 100644 --- a/cli/config.go +++ b/cli/config.go @@ -25,7 +25,6 @@ const ( defChannelsURL string = defURL + ":9005" defGroupsURL string = defURL + ":9004" defHTTPURL string = defURL + ":8008" - defJournalURL string = defURL + ":9021" defTLSVerification bool = false defOffset string = "0" defLimit string = "10" @@ -41,7 +40,6 @@ type remotes struct { GroupsURL string `toml:"groups_url"` HTTPAdapterURL string `toml:"http_adapter_url"` CertsURL string `toml:"certs_url"` - JournalURL string `toml:"journal_url"` HostURL string `toml:"host_url"` TLSVerification bool `toml:"tls_verification"` } @@ -110,7 +108,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) { ChannelsURL: defChannelsURL, GroupsURL: defGroupsURL, HTTPAdapterURL: defHTTPURL, - JournalURL: defJournalURL, HostURL: defURL, TLSVerification: defTLSVerification, }, @@ -194,10 +191,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) { sdkConf.CertsURL = config.Remotes.CertsURL } - if sdkConf.JournalURL == "" && config.Remotes.JournalURL != "" { - sdkConf.JournalURL = config.Remotes.JournalURL - } - if sdkConf.HostURL == "" && config.Remotes.HostURL != "" { sdkConf.HostURL = config.Remotes.HostURL } diff --git a/cli/journal.go b/cli/journal.go deleted file mode 100644 index 669a8f5d6..000000000 --- a/cli/journal.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package cli - -import ( - smqsdk "github.com/absmach/magistrala/pkg/sdk" - "github.com/spf13/cobra" -) - -var cmdJournal = cobra.Command{ - Use: "get ", - Short: "Get journal", - Long: "Get journal\n" + - "Usage:\n" + - "\tmagistrala-cli journal get user - lists user journal logs\n" + - "\tmagistrala-cli journal get - lists entity journal logs\n" + - "\tmagistrala-cli journal get --offset --limit - lists user journal logs with provided offset and limit\n", - Run: func(cmd *cobra.Command, args []string) { - if len(args) < 3 || len(args) > 4 { - logUsageCmd(*cmd, cmd.Use) - return - } - pageMetadata := smqsdk.PageMetadata{ - Offset: Offset, - Limit: Limit, - } - - entityType, entityID, token := args[0], args[1], args[2] - domainID := "" - if len(args) == 4 { - entityType, entityID, domainID, token = args[0], args[1], args[2], args[3] - } - - journal, err := sdk.Journal(cmd.Context(), entityType, entityID, domainID, pageMetadata, token) - if err != nil { - logErrorCmd(*cmd, err) - return - } - - logJSONCmd(*cmd, journal) - }, -} - -// NewJournalCmd returns journal log command. -func NewJournalCmd() *cobra.Command { - cmd := cobra.Command{ - Use: "journal get", - Short: "journal log", - Long: `journal to read journal log`, - } - - cmd.AddCommand(&cmdJournal) - - return &cmd -} diff --git a/cmd/alarms/main.go b/cmd/alarms/main.go deleted file mode 100644 index 92f08e1ed..000000000 --- a/cmd/alarms/main.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "fmt" - "log" - "net/url" - "os" - - "github.com/absmach/magistrala/alarms" - httpAPI "github.com/absmach/magistrala/alarms/api" - "github.com/absmach/magistrala/alarms/brokers" - "github.com/absmach/magistrala/alarms/consumer" - "github.com/absmach/magistrala/alarms/middleware" - "github.com/absmach/magistrala/alarms/operations" - alarmsRepo "github.com/absmach/magistrala/alarms/postgres" - "github.com/absmach/magistrala/internal/atom" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - atomauthn "github.com/absmach/magistrala/pkg/authn/atom" - "github.com/absmach/magistrala/pkg/jaeger" - "github.com/absmach/magistrala/pkg/messaging" - brokerstracing "github.com/absmach/magistrala/pkg/messaging/brokers/tracing" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/pkg/prometheus" - "github.com/absmach/magistrala/pkg/server" - httpserver "github.com/absmach/magistrala/pkg/server/http" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/caarlos0/env/v11" - "golang.org/x/sync/errgroup" -) - -const ( - svcName = "alarms" - envPrefixDB = "MG_ALARMS_DB_" - envPrefixHTTP = "MG_ALARMS_HTTP_" - defDB = "alarms" - defSvcHTTPPort = "8050" - alarmEntity = "alarm" -) - -type config struct { - LogLevel string `env:"MG_ALARMS_LOG_LEVEL" envDefault:"info"` - BrokerURL string `env:"MG_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"` - InstanceID string `env:"MG_ALARMS_INSTANCE_ID" envDefault:""` - JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` - PermissionsFile string `env:"MG_PERMISSIONS_FILE" envDefault:"permission.yaml"` -} - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - cfg := config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("failed to load %s configuration : %s", svcName, err.Error()) - } - - logger, err := mglog.New(os.Stdout, cfg.LogLevel) - if err != nil { - log.Fatalf("failed to init logger: %s", err.Error()) - } - - var exitCode int - defer mglog.ExitWithError(&exitCode) - - tp, err := jaeger.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) - if err != nil { - logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) - exitCode = 1 - return - } - defer func() { - if err := tp.Shutdown(ctx); err != nil { - logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err)) - } - }() - tracer := tp.Tracer(svcName) - - dbConfig := postgres.Config{Name: defDB} - if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { - logger.Error(err.Error()) - } - - migrations, err := alarmsRepo.Migration() - if err != nil { - logger.Error(fmt.Sprintf("failed to load migrations: %s", err)) - exitCode = 1 - return - } - - db, err := postgres.Setup(dbConfig, *migrations) - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - return - } - defer db.Close() - - repo := alarmsRepo.NewAlarmsRepo(db) - - atomCfg := atom.LoadConfig() - if atomCfg.URL == "" { - logger.Error("ATOM_URL is required") - exitCode = 1 - return - } - logger.Info("AuthN configured to use Atom bearer tokens") - logger.Info("AuthZ configured to use Atom PDP") - am := smqauthn.NewAuthNMiddleware(atomauthn.NewAuthentication()) - - idp := uuid.New() - - svc := alarms.NewService(idp, repo) - - permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile) - if err != nil { - logger.Error(fmt.Sprintf("failed to parse permissions file: %s", err)) - exitCode = 1 - return - } - - alarmOps, _, err := permConfig.GetEntityPermissions(alarmEntity) - if err != nil { - logger.Error(fmt.Sprintf("failed to get alarm permissions: %s", err)) - exitCode = 1 - return - } - - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{ - operations.EntityType: alarmOps, - }, - permissions.EntitiesOperationDetails[permissions.Operation]{ - operations.EntityType: operations.OperationDetails(), - }, - ) - if err != nil { - logger.Error(fmt.Sprintf("failed to create entity operations: %s", err)) - exitCode = 1 - return - } - - svc, err = middleware.NewAtomAuthorizationMiddleware(svc, atom.NewClient(atomCfg), entitiesOps) - if err != nil { - logger.Error(fmt.Sprintf("failed to create authorization middleware: %s", err)) - exitCode = 1 - return - } - - svc = middleware.NewLoggingMiddleware(logger, svc) - counter, latency := prometheus.MakeMetrics("alarms", "api") - svc = middleware.NewMetricsMiddleware(counter, latency, svc) - svc = middleware.NewTracingMiddleware(tracer, svc) - - httpServerConfig := server.Config{Port: defSvcHTTPPort} - if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err)) - exitCode = 1 - return - } - hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpAPI.MakeHandler(svc, logger, idp, cfg.InstanceID, am), logger) - - pubSub, err := brokers.NewPubSub(ctx, cfg.BrokerURL, logger) - if err != nil { - logger.Error(fmt.Sprintf("failed to connect to message broker: %s", err)) - exitCode = 1 - return - } - defer pubSub.Close() - pubSub = brokerstracing.NewPubSub(httpServerConfig, tracer, pubSub) - - consumer := consumer.NewHandler(svc, logger) - - subCfg := messaging.SubscriberConfig{ - ID: svcName, - Topic: brokers.AllTopic, - DeliveryPolicy: messaging.DeliverAllPolicy, - Handler: consumer, - } - if err := pubSub.Subscribe(ctx, subCfg); err != nil { - logger.Error(fmt.Sprintf("failed to subscribe to message broker: %s", err)) - exitCode = 1 - - return - } - - g.Go(func() error { - return hs.Start() - }) - - g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName, hs) - }) - - if err := g.Wait(); err != nil { - logger.Error(fmt.Sprintf("billing service terminated: %s", err)) - } -} diff --git a/cmd/atom-bootstrap/main.go b/cmd/atom-bootstrap/main.go index 5bba54cf8..8c86df677 100644 --- a/cmd/atom-bootstrap/main.go +++ b/cmd/atom-bootstrap/main.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" ) const ( diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 91900c8ed..ca78812e0 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -26,9 +26,9 @@ import ( apostgres "github.com/absmach/magistrala/auth/postgres" "github.com/absmach/magistrala/auth/tokenizer/asymmetric" "github.com/absmach/magistrala/auth/tokenizer/symmetric" - "github.com/absmach/magistrala/internal/atom" redisclient "github.com/absmach/magistrala/internal/clients/redis" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/policies" pgclient "github.com/absmach/magistrala/pkg/postgres" diff --git a/cmd/certs/main.go b/cmd/certs/main.go index c5fed4919..2fae17e8b 100644 --- a/cmd/certs/main.go +++ b/cmd/certs/main.go @@ -20,8 +20,8 @@ import ( "github.com/absmach/magistrala/certs/middleware" "github.com/absmach/magistrala/certs/pki" "github.com/absmach/magistrala/certs/postgres" - "github.com/absmach/magistrala/internal/atom" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/atom" smqauthn "github.com/absmach/magistrala/pkg/authn" atomauthn "github.com/absmach/magistrala/pkg/authn/atom" smqauthz "github.com/absmach/magistrala/pkg/authz" diff --git a/cmd/fluxmq/main.go b/cmd/fluxmq/main.go index 1679137ed..346d4826f 100644 --- a/cmd/fluxmq/main.go +++ b/cmd/fluxmq/main.go @@ -22,8 +22,8 @@ import ( "github.com/absmach/fluxmq/pkg/proto/auth/v1/authv1connect" fluxmqgrpc "github.com/absmach/magistrala/fluxmq/api/grpc" fluxmqhttp "github.com/absmach/magistrala/fluxmq/api/http" - "github.com/absmach/magistrala/internal/atom" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/atom" atomauthn "github.com/absmach/magistrala/pkg/authn/atom" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/messaging" diff --git a/cmd/journal/main.go b/cmd/journal/main.go deleted file mode 100644 index fad88edea..000000000 --- a/cmd/journal/main.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package main contains journal main function to start the journal service. -package main - -import ( - "context" - "fmt" - "log" - "log/slog" - "net/url" - "os" - - chclient "github.com/absmach/callhome/pkg/client" - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/journal" - httpapi "github.com/absmach/magistrala/journal/api" - "github.com/absmach/magistrala/journal/events" - "github.com/absmach/magistrala/journal/middleware" - journalpg "github.com/absmach/magistrala/journal/postgres" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - atomauthn "github.com/absmach/magistrala/pkg/authn/atom" - smqauthz "github.com/absmach/magistrala/pkg/authz" - "github.com/absmach/magistrala/pkg/events/store" - jaegerclient "github.com/absmach/magistrala/pkg/jaeger" - "github.com/absmach/magistrala/pkg/postgres" - pgclient "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/pkg/prometheus" - "github.com/absmach/magistrala/pkg/server" - "github.com/absmach/magistrala/pkg/server/http" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/caarlos0/env/v11" - "github.com/jmoiron/sqlx" - "go.opentelemetry.io/otel/trace" - "golang.org/x/sync/errgroup" -) - -const ( - svcName = "journal" - envPrefixDB = "MG_JOURNAL_DB_" - envPrefixHTTP = "MG_JOURNAL_HTTP_" - defDB = "journal" - defSvcHTTPPort = "9021" -) - -type config struct { - LogLevel string `env:"MG_JOURNAL_LOG_LEVEL" envDefault:"info"` - ESURL string `env:"MG_ES_URL" envDefault:"amqp://guest:guest@localhost:5682/"` - JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` - InstanceID string `env:"MG_JOURNAL_INSTANCE_ID" envDefault:""` - TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` -} - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - cfg := config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("failed to load %s configuration : %s", svcName, err) - } - - logger, err := mglog.New(os.Stdout, cfg.LogLevel) - if err != nil { - log.Fatalf("failed to init logger: %s", err) - } - - var exitCode int - defer mglog.ExitWithError(&exitCode) - - if cfg.InstanceID == "" { - if cfg.InstanceID, err = uuid.New().ID(); err != nil { - logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) - exitCode = 1 - return - } - } - - dbConfig := pgclient.Config{Name: defDB} - if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { - logger.Error(err.Error()) - exitCode = 1 - return - } - db, err := pgclient.Setup(dbConfig, *journalpg.Migration()) - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - return - } - defer db.Close() - - atomCfg := atom.LoadConfig() - if atomCfg.URL == "" { - logger.Error("ATOM_URL is required") - exitCode = 1 - return - } - atomClient := atom.NewClient(atomCfg) - authn := atomauthn.NewAuthentication() - authnMiddleware := smqauthn.NewAuthNMiddleware(authn) - authz := atom.NewAuthorizationCompat(atomClient) - logger.Info("AuthN/AuthZ configured to use Atom") - - tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) - if err != nil { - logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) - exitCode = 1 - return - } - defer func() { - if err := tp.Shutdown(ctx); err != nil { - logger.Error(fmt.Sprintf("error shutting down tracer provider: %s", err)) - } - }() - tracer := tp.Tracer(svcName) - - svc := newService(db, dbConfig, authz, logger, tracer) - - subscriber, err := store.NewSubscriber(ctx, cfg.ESURL, "journal-es-sub", logger) - if err != nil { - logger.Error(fmt.Sprintf("failed to create subscriber: %s", err)) - exitCode = 1 - return - } - - logger.Info("Subscribed to Event Store") - - if err := events.Start(ctx, svcName, subscriber, svc); err != nil { - logger.Error("failed to start %s service: %s", svcName, err) - exitCode = 1 - return - } - - httpServerConfig := server.Config{Port: defSvcHTTPPort} - if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) - exitCode = 1 - return - } - - hs := http.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authnMiddleware, logger, svcName, cfg.InstanceID), logger) - - if cfg.SendTelemetry { - chc := chclient.New(svcName, magistrala.Version, logger, cancel) - go chc.CallHome(ctx) - } - - g.Go(func() error { - return hs.Start() - }) - - g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName, hs) - }) - - if err := g.Wait(); err != nil { - logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err)) - } -} - -func newService(db *sqlx.DB, dbConfig pgclient.Config, authz smqauthz.Authorization, logger *slog.Logger, tracer trace.Tracer) journal.Service { - database := postgres.NewDatabase(db, dbConfig, tracer) - repo := journalpg.NewRepository(database) - idp := uuid.New() - - svc := journal.NewService(idp, repo) - svc = middleware.NewAuthorization(svc, authz) - svc = middleware.NewLogging(svc, logger) - counter, latency := prometheus.MakeMetrics("journal", "journal_writer") - svc = middleware.NewMetrics(svc, counter, latency) - svc = middleware.NewTracing(svc, tracer) - - return svc -} diff --git a/cmd/notifications/main.go b/cmd/notifications/main.go deleted file mode 100644 index 7e3128953..000000000 --- a/cmd/notifications/main.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package main contains notifications main function to start the notifications service. -package main - -import ( - "context" - "fmt" - "log" - "net/url" - "os" - - chclient "github.com/absmach/callhome/pkg/client" - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/internal/atom" - mglog "github.com/absmach/magistrala/logger" - "github.com/absmach/magistrala/notifications/emailer" - "github.com/absmach/magistrala/notifications/events" - "github.com/absmach/magistrala/notifications/middleware" - "github.com/absmach/magistrala/pkg/events/store" - jaegerclient "github.com/absmach/magistrala/pkg/jaeger" - "github.com/absmach/magistrala/pkg/prometheus" - "github.com/absmach/magistrala/pkg/server" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/caarlos0/env/v11" - "golang.org/x/sync/errgroup" -) - -const ( - svcName = "notifications" - defEmailPort = "25" -) - -type config struct { - LogLevel string `env:"MG_NOTIFICATIONS_LOG_LEVEL" envDefault:"info"` - ESURL string `env:"MG_ES_URL" envDefault:"amqp://guest:guest@localhost:5682/"` - JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` - InstanceID string `env:"MG_NOTIFICATIONS_INSTANCE_ID" envDefault:""` - DomainAltName string `env:"MG_NOTIFICATIONS_DOMAIN_ALT_NAME" envDefault:"domain"` - TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` - EmailHost string `env:"MG_EMAIL_HOST" envDefault:"localhost"` - EmailPort string `env:"MG_EMAIL_PORT" envDefault:"25"` - EmailUsername string `env:"MG_EMAIL_USERNAME" envDefault:""` - EmailPassword string `env:"MG_EMAIL_PASSWORD" envDefault:""` - EmailFromAddress string `env:"MG_EMAIL_FROM_ADDRESS" envDefault:"noreply@magistrala.com"` - EmailFromName string `env:"MG_EMAIL_FROM_NAME" envDefault:"Magistrala Notifications"` - InvitationTemplate string `env:"MG_EMAIL_INVITATION_TEMPLATE" envDefault:"docker/templates/invitation-sent-email.tmpl"` - AcceptanceTemplate string `env:"MG_EMAIL_ACCEPTANCE_TEMPLATE" envDefault:"docker/templates/invitation-accepted-email.tmpl"` - RejectionTemplate string `env:"MG_EMAIL_REJECTION_TEMPLATE" envDefault:"docker/templates/invitation-rejected-email.tmpl"` -} - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - cfg := config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("failed to load %s configuration : %s", svcName, err) - } - - logger, err := mglog.New(os.Stdout, cfg.LogLevel) - if err != nil { - log.Fatalf("failed to init logger: %s", err) - } - - var exitCode int - defer mglog.ExitWithError(&exitCode) - - if cfg.InstanceID == "" { - if cfg.InstanceID, err = uuid.New().ID(); err != nil { - logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) - exitCode = 1 - return - } - } - - tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) - if err != nil { - logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) - exitCode = 1 - return - } - defer func() { - if err := tp.Shutdown(ctx); err != nil { - logger.Error(fmt.Sprintf("error shutting down tracer provider: %s", err)) - } - }() - - atomCfg := atom.LoadConfig() - if atomCfg.URL == "" { - logger.Error("ATOM_URL is required") - exitCode = 1 - return - } - usersResolver := emailer.NewAtomUserResolver(atom.NewClient(atomCfg)) - logger.Info("Notifications user lookup configured to use Atom") - - emailerCfg := emailer.Config{ - FromAddress: cfg.EmailFromAddress, - FromName: cfg.EmailFromName, - DomainAltName: cfg.DomainAltName, - InvitationTemplate: cfg.InvitationTemplate, - AcceptanceTemplate: cfg.AcceptanceTemplate, - RejectionTemplate: cfg.RejectionTemplate, - EmailHost: cfg.EmailHost, - EmailPort: cfg.EmailPort, - EmailUsername: cfg.EmailUsername, - EmailPassword: cfg.EmailPassword, - } - - notifier, err := emailer.New(usersResolver, emailerCfg) - if err != nil { - logger.Error(fmt.Sprintf("failed to create emailer: %s", err)) - exitCode = 1 - return - } - - // Wrap notifier with middleware - notifier = middleware.NewLogging(notifier, logger) - counter, latency := prometheus.MakeMetrics(svcName, "notifier") - notifier = middleware.NewMetrics(notifier, counter, latency) - notifier = middleware.NewTracing(notifier, tp.Tracer(svcName)) - - subscriber, err := store.NewSubscriber(ctx, cfg.ESURL, "notifications-es-sub", logger) - if err != nil { - logger.Error(fmt.Sprintf("failed to create subscriber: %s", err)) - exitCode = 1 - return - } - - logger.Info("Subscribed to Event Store") - - if err := events.Start(ctx, svcName, subscriber, notifier); err != nil { - logger.Error(fmt.Sprintf("failed to start %s service: %s", svcName, err)) - exitCode = 1 - return - } - - if cfg.SendTelemetry { - chc := chclient.New(svcName, magistrala.Version, logger, cancel) - go chc.CallHome(ctx) - } - - g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName) - }) - - if err := g.Wait(); err != nil { - logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err)) - } -} diff --git a/cmd/postgres-reader/main.go b/cmd/postgres-reader/main.go index 0827bf51f..093f5d550 100644 --- a/cmd/postgres-reader/main.go +++ b/cmd/postgres-reader/main.go @@ -14,8 +14,8 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/internal/atom" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/atom" atomauthn "github.com/absmach/magistrala/pkg/authn/atom" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" diff --git a/cmd/postgres-writer/main.go b/cmd/postgres-writer/main.go index ae43f9a28..e8ece3e30 100644 --- a/cmd/postgres-writer/main.go +++ b/cmd/postgres-writer/main.go @@ -17,11 +17,11 @@ import ( "github.com/absmach/magistrala/consumers" consumertracing "github.com/absmach/magistrala/consumers/tracing" httpapi "github.com/absmach/magistrala/consumers/writers/api" - "github.com/absmach/magistrala/consumers/writers/brokers" writerpg "github.com/absmach/magistrala/consumers/writers/postgres" mglog "github.com/absmach/magistrala/logger" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" brokerstracing "github.com/absmach/magistrala/pkg/messaging/brokers/tracing" + brokers "github.com/absmach/magistrala/pkg/messaging/writers" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" "github.com/absmach/magistrala/pkg/server" diff --git a/cmd/re/main.go b/cmd/re/main.go deleted file mode 100644 index 09caf3691..000000000 --- a/cmd/re/main.go +++ /dev/null @@ -1,348 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package main contains rule engine main function to start the service. -package main - -import ( - "context" - "fmt" - "log" - "log/slog" - "net/url" - "os" - "time" - - chclient "github.com/absmach/callhome/pkg/client" - "github.com/absmach/magistrala" - abrokers "github.com/absmach/magistrala/alarms/brokers" - grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/consumers/writers/brokers" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/internal/email" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - atomauthn "github.com/absmach/magistrala/pkg/authn/atom" - "github.com/absmach/magistrala/pkg/callout" - "github.com/absmach/magistrala/pkg/emailer" - "github.com/absmach/magistrala/pkg/grpcclient" - jaegerclient "github.com/absmach/magistrala/pkg/jaeger" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - smqbrokers "github.com/absmach/magistrala/pkg/messaging/brokers" - brokerstracing "github.com/absmach/magistrala/pkg/messaging/brokers/tracing" - "github.com/absmach/magistrala/pkg/permissions" - pgclient "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/pkg/prometheus" - "github.com/absmach/magistrala/pkg/server" - httpserver "github.com/absmach/magistrala/pkg/server/http" - "github.com/absmach/magistrala/pkg/ticker" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/absmach/magistrala/re" - httpapi "github.com/absmach/magistrala/re/api" - "github.com/absmach/magistrala/re/events" - "github.com/absmach/magistrala/re/middleware" - "github.com/absmach/magistrala/re/operations" - repg "github.com/absmach/magistrala/re/postgres" - grpcClient "github.com/absmach/magistrala/readers/api/grpc" - "github.com/caarlos0/env/v11" - "github.com/go-chi/chi/v5" - "go.opentelemetry.io/otel/trace" - "golang.org/x/sync/errgroup" -) - -const ( - svcName = "rules_engine" - envPrefixDB = "MG_RE_DB_" - envPrefixHTTP = "MG_RE_HTTP_" - envPrefixCallout = "MG_RE_CALLOUT_" - defDB = "r" - defSvcHTTPPort = "9008" - envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_" -) - -// We use a buffered channel to prevent blocking, as logging is an expensive operation. -// A larger buffer size would also work, but we’d likely need another instance of RE in that case. -// A smaller size would probably work too, but there's no need to be that frugal with resources. -const channBuffer = 256 - -type config struct { - LogLevel string `env:"MG_RE_LOG_LEVEL" envDefault:"info"` - InstanceID string `env:"MG_RE_INSTANCE_ID" envDefault:""` - JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` - ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"` - ESConsumerName string `env:"MG_RE_EVENT_CONSUMER" envDefault:"rules_engine"` - CacheURL string `env:"MG_RE_CACHE_URL" envDefault:"redis://localhost:6379/0"` - CacheKeyDuration time.Duration `env:"MG_RE_CACHE_KEY_DURATION" envDefault:"10m"` - TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` - BrokerURL string `env:"MG_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"` - PermissionsFile string `env:"MG_PERMISSIONS_FILE" envDefault:"permission.yaml"` -} - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - cfg := config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("failed to load %s configuration : %s", svcName, err) - } - - var logger *slog.Logger - logger, err := mglog.New(os.Stdout, cfg.LogLevel) - if err != nil { - log.Fatalf("failed to init logger: %s", err.Error()) - } - - var exitCode int - defer mglog.ExitWithError(&exitCode) - - if cfg.InstanceID == "" { - if cfg.InstanceID, err = uuid.New().ID(); err != nil { - logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) - exitCode = 1 - return - } - } - - ec := email.Config{} - if err := env.Parse(&ec); err != nil { - logger.Error(fmt.Sprintf("failed to load email configuration : %s", err)) - exitCode = 1 - - return - } - - callCfg := callout.Config{} - if err := env.ParseWithOptions(&callCfg, env.Options{Prefix: envPrefixCallout}); err != nil { - logger.Error(fmt.Sprintf("failed to parse callout config : %s", err)) - exitCode = 1 - return - } - - dbConfig := pgclient.Config{Name: defDB} - if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - migration, err := repg.Migration() - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - db, err := pgclient.Setup(dbConfig, *migration) - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - defer db.Close() - - tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) - if err != nil { - logger.Error(fmt.Sprintf("Failed to init Jaeger: %s", err)) - exitCode = 1 - - return - } - defer func() { - if err := tp.Shutdown(ctx); err != nil { - logger.Error(fmt.Sprintf("Error shutting down tracer provider: %v", err)) - } - }() - tracer := tp.Tracer(svcName) - - httpServerConfig := server.Config{Port: defSvcHTTPPort} - if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err)) - exitCode = 1 - - return - } - - callout, err := callout.New(callCfg) - if err != nil { - logger.Error(fmt.Sprintf("failed to create new callout: %s", err)) - exitCode = 1 - return - } - - msgSub, err := smqbrokers.NewPubSub(ctx, cfg.BrokerURL, logger, smqbrokers.ConnectionName("re-msg-pubsub")) - if err != nil { - logger.Error(fmt.Sprintf("failed to connect to message broker for mg pubSub: %s", err)) - exitCode = 1 - - return - } - defer msgSub.Close() - msgSub = brokerstracing.NewPubSub(httpServerConfig, tracer, msgSub) - - writersPub, err := brokers.NewPublisher(ctx, cfg.BrokerURL) - if err != nil { - logger.Error(fmt.Sprintf("failed to connect to message broker for writers publisher: %s", err)) - exitCode = 1 - - return - } - defer writersPub.Close() - writersPub = brokerstracing.NewPublisher(httpServerConfig, tracer, writersPub) - - alarmsPub, err := abrokers.NewPublisher(ctx, cfg.BrokerURL) - if err != nil { - logger.Error(fmt.Sprintf("failed to connect to message broker for alarms publisher: %s", err)) - exitCode = 1 - - return - } - defer alarmsPub.Close() - alarmsPub = brokerstracing.NewPublisher(httpServerConfig, tracer, alarmsPub) - - atomCfg := atom.LoadConfig() - if atomCfg.URL == "" { - logger.Error("ATOM_URL is required") - exitCode = 1 - return - } - authnSvc := atomauthn.NewAuthentication() - logger.Info("AuthN configured to use Atom bearer tokens") - am := smqauthn.NewAuthNMiddleware(authnSvc) - runInfo := make(chan pkglog.RunInfo, channBuffer) - - logger.Info("AuthZ configured to use Atom PDP") - - database := pgclient.NewDatabase(db, dbConfig, tracer) - regrpcCfg := grpcclient.Config{} - if err := env.ParseWithOptions(®rpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil { - logger.Error(fmt.Sprintf("failed to load clients gRPC client configuration : %s", err)) - exitCode = 1 - return - } - - client, err := grpcclient.NewHandler(regrpcCfg) - if err != nil { - exitCode = 1 - return - } - defer client.Close() - - readersClient := grpcClient.NewReadersClient(client.Connection(), regrpcCfg.Timeout) - logger.Info("Readers gRPC client successfully connected to readers gRPC server " + client.Secure()) - - svc, err := newService(ctx, cfg, database, runInfo, msgSub, writersPub, alarmsPub, ec, logger, readersClient, callout, tracer) - if err != nil { - logger.Error(fmt.Sprintf("failed to create services: %s", err)) - exitCode = 1 - - return - } - subCfg := messaging.SubscriberConfig{ - ID: svcName, - Topic: smqbrokers.SubjectAllMessages, - DeliveryPolicy: messaging.DeliverAllPolicy, - Handler: svc, - } - if err := msgSub.Subscribe(ctx, subCfg); err != nil { - logger.Error(fmt.Sprintf("failed to subscribe to internal message broker: %s", err)) - exitCode = 1 - - return - } - - go func() { - for info := range runInfo { - logger.LogAttrs(context.Background(), info.Level, info.Message, info.Details...) - } - }() - - mux := chi.NewRouter() - - httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, mux, logger, cfg.InstanceID), logger) - - if cfg.SendTelemetry { - chc := chclient.New(svcName, magistrala.Version, logger, cancel) - go chc.CallHome(ctx) - } - - g.Go(func() error { - return svc.StartScheduler(ctx) - }) - - g.Go(func() error { - return httpSvc.Start() - }) - - g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSvc) - }) - - if err := g.Wait(); err != nil { - logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err)) - } -} - -func newService(ctx context.Context, cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, callout callout.Callout, tracer trace.Tracer) (re.Service, error) { - repo := repg.NewRepository(db) - idp := uuid.New() - - emailerClient, err := emailer.New(&ec) - if err != nil { - logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error())) - } - - atomCfg := atom.LoadConfig() - - var csvc re.Service - csvc, err = re.NewService(repo, runInfo, idp, rePubSub, writersPub, alarmsPub, ticker.NewTicker(time.Second*30), emailerClient, readersClient) - if err != nil { - return nil, fmt.Errorf("failed to create RE service: %w", err) - } - csvc = re.WithAtom(csvc, atom.NewClient(atomCfg)) - - csvc, err = events.NewEventStoreMiddleware(ctx, csvc, cfg.ESURL) - if err != nil { - return nil, fmt.Errorf("failed to init re event store middleware: %w", err) - } - - permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile) - if err != nil { - return nil, fmt.Errorf("failed to parse permissions file: %w", err) - } - - ruleOps, _, err := permConfig.GetEntityPermissions(operations.EntityType) - if err != nil { - return nil, fmt.Errorf("failed to get rule permissions: %w", err) - } - - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{ - operations.EntityType: ruleOps, - }, - permissions.EntitiesOperationDetails[permissions.Operation]{ - operations.EntityType: operations.OperationDetails(), - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create entities operations: %w", err) - } - - csvc, err = middleware.AtomAuthorizationMiddleware(csvc, atom.NewClient(atomCfg), entitiesOps) - if err != nil { - return nil, err - } - csvc, err = middleware.NewCallout(csvc, callout, entitiesOps) - if err != nil { - return nil, err - } - csvc = middleware.LoggingMiddleware(csvc, logger) - counter, latency := prometheus.MakeMetrics("re", "api") - csvc = middleware.NewMetricsMiddleware(counter, latency, csvc) - csvc = middleware.NewTracingMiddleware(tracer, csvc) - - return csvc, nil -} diff --git a/cmd/reports/main.go b/cmd/reports/main.go deleted file mode 100644 index b5391bfc4..000000000 --- a/cmd/reports/main.go +++ /dev/null @@ -1,331 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package main contains reports main function to start the service. -package main - -import ( - "context" - "embed" - "fmt" - "log" - "log/slog" - "net/url" - "os" - "time" - - chclient "github.com/absmach/callhome/pkg/client" - "github.com/absmach/magistrala" - grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/internal/email" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - atomauthn "github.com/absmach/magistrala/pkg/authn/atom" - "github.com/absmach/magistrala/pkg/callout" - "github.com/absmach/magistrala/pkg/emailer" - "github.com/absmach/magistrala/pkg/grpcclient" - jaegerclient "github.com/absmach/magistrala/pkg/jaeger" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/permissions" - pgclient "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/pkg/prometheus" - "github.com/absmach/magistrala/pkg/server" - httpserver "github.com/absmach/magistrala/pkg/server/http" - "github.com/absmach/magistrala/pkg/ticker" - "github.com/absmach/magistrala/pkg/uuid" - grpcClient "github.com/absmach/magistrala/readers/api/grpc" - "github.com/absmach/magistrala/reports" - httpapi "github.com/absmach/magistrala/reports/api" - reportsevents "github.com/absmach/magistrala/reports/events" - "github.com/absmach/magistrala/reports/middleware" - "github.com/absmach/magistrala/reports/operations" - repg "github.com/absmach/magistrala/reports/postgres" - "github.com/caarlos0/env/v11" - "github.com/go-chi/chi/v5" - "go.opentelemetry.io/otel/trace" - "golang.org/x/sync/errgroup" -) - -const ( - svcName = "reports" - envPrefixDB = "MG_REPORTS_DB_" - envPrefixHTTP = "MG_REPORTS_HTTP_" - envPrefixCallout = "MG_REPORTS_CALLOUT_" - defDB = "repo" - defSvcHTTPPort = "9017" - envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_" - templatePath = "template/reports_default_template.html" - reportEntity = "report" -) - -// We use a buffered channel to prevent blocking, as logging is an expensive operation. -const channBuffer = 256 - -//go:embed template/reports_default_template.html -var templateFS embed.FS - -type config struct { - LogLevel string `env:"MG_REPORTS_LOG_LEVEL" envDefault:"info"` - InstanceID string `env:"MG_REPORTS_INSTANCE_ID" envDefault:""` - JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` - ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"` - ESConsumerName string `env:"MG_REPORTS_EVENT_CONSUMER" envDefault:"reports"` - TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` - BrokerURL string `env:"MG_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"` - DefaultTemplatePath string `env:"MG_REPORTS_DEFAULT_TEMPLATE" envDefault:""` - ConverterURL string `env:"MG_PDF_CONVERTER_URL" envDefault:"http://localhost:4000/pdf"` - PermissionsFile string `env:"MG_PERMISSIONS_FILE" envDefault:"permission.yaml"` -} - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - cfg := config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("failed to load %s configuration : %s", svcName, err) - } - - var logger *slog.Logger - logger, err := mglog.New(os.Stdout, cfg.LogLevel) - if err != nil { - log.Fatalf("failed to init logger: %s", err.Error()) - } - - var exitCode int - defer mglog.ExitWithError(&exitCode) - - if cfg.InstanceID == "" { - if cfg.InstanceID, err = uuid.New().ID(); err != nil { - logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) - exitCode = 1 - return - } - } - - var templateData []byte - - switch cfg.DefaultTemplatePath { - case "": - templateData, err = templateFS.ReadFile(templatePath) - default: - templateData, err = os.ReadFile(templatePath) - } - - if err != nil { - logger.Error(fmt.Sprintf("failed to read report template: %s", err)) - exitCode = 1 - return - } - - template := reports.ReportTemplate(string(templateData)) - - if err := template.Validate(); err != nil { - logger.Error(fmt.Sprintf("failed to validate report template: %s", err)) - exitCode = 1 - return - } - logger.Info("Report template validated successfully") - - ec := email.Config{} - if err := env.Parse(&ec); err != nil { - logger.Error(fmt.Sprintf("failed to load email configuration : %s", err)) - exitCode = 1 - - return - } - - callCfg := callout.Config{} - if err := env.ParseWithOptions(&callCfg, env.Options{Prefix: envPrefixCallout}); err != nil { - logger.Error(fmt.Sprintf("failed to parse callout config : %s", err)) - exitCode = 1 - return - } - - dbConfig := pgclient.Config{Name: defDB} - if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - - migration, err := repg.Migration() - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - - db, err := pgclient.Setup(dbConfig, *migration) - if err != nil { - logger.Error(err.Error()) - exitCode = 1 - - return - } - defer db.Close() - - tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) - if err != nil { - logger.Error(fmt.Sprintf("Failed to init Jaeger: %s", err)) - exitCode = 1 - - return - } - defer func() { - if err := tp.Shutdown(ctx); err != nil { - logger.Error(fmt.Sprintf("Error shutting down tracer provider: %v", err)) - } - }() - tracer := tp.Tracer(svcName) - - httpServerConfig := server.Config{Port: defSvcHTTPPort} - if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err)) - exitCode = 1 - - return - } - - callout, err := callout.New(callCfg) - if err != nil { - logger.Error(fmt.Sprintf("failed to create new callout: %s", err)) - exitCode = 1 - return - } - - atomCfg := atom.LoadConfig() - if atomCfg.URL == "" { - logger.Error("ATOM_URL is required") - exitCode = 1 - return - } - authnSvc := atomauthn.NewAuthentication() - logger.Info("AuthN configured to use Atom bearer tokens") - am := smqauthn.NewAuthNMiddleware(authnSvc) - - logger.Info("AuthZ configured to use Atom PDP") - database := pgclient.NewDatabase(db, dbConfig, tracer) - regrpcCfg := grpcclient.Config{} - if err := env.ParseWithOptions(®rpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil { - logger.Error(fmt.Sprintf("failed to load clients gRPC client configuration : %s", err)) - exitCode = 1 - return - } - - client, err := grpcclient.NewHandler(regrpcCfg) - if err != nil { - exitCode = 1 - return - } - defer client.Close() - - readersClient := grpcClient.NewReadersClient(client.Connection(), regrpcCfg.Timeout) - logger.Info("Readers gRPC client successfully connected to readers gRPC server " + client.Secure()) - - runInfo := make(chan pkglog.RunInfo, channBuffer) - - svc, err := newService(ctx, cfg, database, runInfo, ec, logger, readersClient, template, callout, tracer) - if err != nil { - logger.Error(fmt.Sprintf("failed to create services: %s", err)) - exitCode = 1 - - return - } - - go func() { - for info := range runInfo { - logger.LogAttrs(context.Background(), info.Level, info.Message, info.Details...) - } - }() - - mux := chi.NewRouter() - - httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, mux, logger, cfg.InstanceID), logger) - - if cfg.SendTelemetry { - chc := chclient.New(svcName, magistrala.Version, logger, cancel) - go chc.CallHome(ctx) - } - - g.Go(func() error { - return svc.StartScheduler(ctx) - }) - - g.Go(func() error { - return httpSvc.Start() - }) - - g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSvc) - }) - - if err := g.Wait(); err != nil { - logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err)) - } -} - -func newService(ctx context.Context, cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, callout callout.Callout, tracer trace.Tracer) (reports.Service, error) { - repo := repg.NewRepository(db) - idp := uuid.New() - - emailClient, err := emailer.New(&ec) - if err != nil { - logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error())) - } - - atomCfg := atom.LoadConfig() - - var csvc reports.Service - csvc, err = reports.NewService(repo, runInfo, idp, ticker.NewTicker(time.Second*30), emailClient, readersClient, template, cfg.ConverterURL) - if err != nil { - return nil, fmt.Errorf("failed to create reports service: %w", err) - } - csvc = reports.WithAtom(csvc, atom.NewClient(atomCfg)) - - csvc, err = reportsevents.NewEventStoreMiddleware(ctx, csvc, cfg.ESURL) - if err != nil { - return nil, fmt.Errorf("failed to init reports event store middleware: %w", err) - } - - permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile) - if err != nil { - return nil, fmt.Errorf("failed to parse permissions file: %w", err) - } - - reportOps, _, err := permConfig.GetEntityPermissions(reportEntity) - if err != nil { - return nil, fmt.Errorf("failed to get report permissions: %w", err) - } - - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{ - operations.EntityType: reportOps, - }, - permissions.EntitiesOperationDetails[permissions.Operation]{ - operations.EntityType: operations.OperationDetails(), - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create entities operations: %w", err) - } - - csvc, err = middleware.AtomAuthorizationMiddleware(csvc, atom.NewClient(atomCfg), entitiesOps) - if err != nil { - return nil, err - } - csvc, err = middleware.NewCallout(csvc, callout, entitiesOps) - if err != nil { - return nil, err - } - csvc = middleware.LoggingMiddleware(csvc, logger) - counter, latency := prometheus.MakeMetrics("reports", "api") - csvc = middleware.NewMetricsMiddleware(counter, latency, csvc) - csvc = middleware.NewTracingMiddleware(tracer, csvc) - - return csvc, nil -} diff --git a/cmd/reports/template/reports_default_template.html b/cmd/reports/template/reports_default_template.html deleted file mode 100644 index 1785e0931..000000000 --- a/cmd/reports/template/reports_default_template.html +++ /dev/null @@ -1,479 +0,0 @@ - - - - - - - - - {{.Title}} - - - - {{if gt (len .Reports) 0}} - {{$firstPageRows := 24}} - {{$continuationPageRows := 32}} - {{$totalPages := 0}} - - {{/* Calculate total pages across all reports */}} - {{range $report := .Reports}} - {{$totalMessages := len .Messages}} - {{$reportPages := 1}} - {{if gt $totalMessages $firstPageRows}} - {{$remaining := sub $totalMessages $firstPageRows}} - {{$additionalPages := div $remaining $continuationPageRows}} - {{if gt (mod $remaining $continuationPageRows) 0}} - {{$additionalPages = add $additionalPages 1}} - {{end}} - {{$reportPages = add 1 $additionalPages}} - {{end}} - {{$totalPages = add $totalPages $reportPages}} - {{end}} - - {{$globalPage := 0}} - - {{range $reportIndex, $report := .Reports}} - {{$totalMessages := len .Messages}} - {{$pageCount := 1}} - {{if gt $totalMessages $firstPageRows}} - {{$remaining := sub $totalMessages $firstPageRows}} - {{$additionalPages := div $remaining $continuationPageRows}} - {{if gt (mod $remaining $continuationPageRows) 0}} - {{$additionalPages = add $additionalPages 1}} - {{end}} - {{$pageCount = add 1 $additionalPages}} - {{end}} - - {{range $pageNum := iterate $pageCount}} - {{$globalPage = add $globalPage 1}} - {{$isFirstPage := eq $pageNum 0}} - {{$startRow := getStartRow $pageNum $firstPageRows $continuationPageRows}} - {{$endRow := getEndRow $pageNum $firstPageRows $continuationPageRows $totalMessages}} - -
-
-
-
-
-
{{$.Title}}
-
{{$.GeneratedDate}}{{if $.Timezone}} ({{$.Timezone}}){{end}}
-
-
-
- -
- {{if $isFirstPage}} -
-
Metrics
-
-
-
Name:
-
{{$report.Metric.Name}}
-
- {{if $report.Metric.ClientID}} -
-
Device ID:
-
{{$report.Metric.ClientID}}
-
- {{end}} -
-
Channel ID:
-
{{$report.Metric.ChannelID}}
-
-
-
- -
- Total Records: {{$totalMessages}} -
- {{else}} -
-
Metrics (continued)
-
- {{end}} - -
-
- - - - - - - - - - - - {{range $msgIndex, $msg := $report.Messages}} - {{if and (ge $msgIndex $startRow) (lt $msgIndex $endRow)}} - - - - - - - - {{end}} - {{end}} - -
TimeValueUnitProtocolSubtopic
{{formatTime $msg.Time}}{{formatValue $msg}}{{$msg.Unit}}{{$msg.Protocol}}{{$msg.Subtopic}}
-
-
- - -
- {{end}} - {{end}} - {{else}} -
-
-
-
-
-
{{.Title}}
-
{{.GeneratedDate}}{{if .Timezone}} ({{.Timezone}}){{end}}
-
-
-
- -
-
-
Metrics
-
-
-
Name:
-
No Report
-
-
-
Channel ID:
-
N/A
-
-
-
- -
- Total Records: 0 -
- -
-
- - - - - - - - - - - - - - - -
TimeValueUnitProtocolSubtopic
No data available
-
-
- - -
- {{end}} - - - diff --git a/cmd/timescale-reader/main.go b/cmd/timescale-reader/main.go index a99d4c3e9..f79078def 100644 --- a/cmd/timescale-reader/main.go +++ b/cmd/timescale-reader/main.go @@ -14,8 +14,8 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/internal/atom" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/atom" atomauthn "github.com/absmach/magistrala/pkg/authn/atom" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" diff --git a/cmd/timescale-writer/main.go b/cmd/timescale-writer/main.go index 80a4d2bce..b675ddaec 100644 --- a/cmd/timescale-writer/main.go +++ b/cmd/timescale-writer/main.go @@ -17,11 +17,11 @@ import ( "github.com/absmach/magistrala/consumers" consumertracing "github.com/absmach/magistrala/consumers/tracing" httpapi "github.com/absmach/magistrala/consumers/writers/api" - "github.com/absmach/magistrala/consumers/writers/brokers" "github.com/absmach/magistrala/consumers/writers/timescale" mglog "github.com/absmach/magistrala/logger" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" brokerstracing "github.com/absmach/magistrala/pkg/messaging/brokers/tracing" + brokers "github.com/absmach/magistrala/pkg/messaging/writers" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" "github.com/absmach/magistrala/pkg/server" diff --git a/config.toml b/config.toml index cbfa63773..5474aeae5 100644 --- a/config.toml +++ b/config.toml @@ -17,6 +17,5 @@ user_token = "" groups_url = "http://localhost:9004" host_url = "http://localhost" http_adapter_url = "http://localhost:8008" - journal_url = "http://localhost:9021" tls_verification = false users_url = "http://localhost:9002" diff --git a/consumers/notifiers/README.md b/consumers/notifiers/README.md index ecfdc8224..70e50a70d 100644 --- a/consumers/notifiers/README.md +++ b/consumers/notifiers/README.md @@ -8,7 +8,7 @@ The service is configured using environment variables. Values shown are from [do ### SMTP notifier (email) -Used by `consumers/notifiers/smtp` via `internal/email`. +Used by `consumers/notifiers/smtp` via `pkg/email`. | Variable | Description | Default | | ----------------------- | ---------------------------------------------- | ------------------ | @@ -93,7 +93,7 @@ Defined in `consumers/notifiers/smpp/README.md`. - **Service layer**: `consumers/notifiers/service.go` handles authn, ID creation, and notification dispatch. - **Repository**: `consumers/notifiers/postgres` persists subscriptions and supports filtering. - **Notifier implementations**: `consumers/notifiers/smtp` (email) and `consumers/notifiers/smpp` (SMS). -- **Email agent**: `internal/email` manages SMTP connectivity and template rendering. +- **Email agent**: `pkg/email` manages SMTP connectivity and template rendering. ### Subscriptions table diff --git a/consumers/notifiers/api/endpoint_test.go b/consumers/notifiers/api/endpoint_test.go index a24c63c23..c61d56447 100644 --- a/consumers/notifiers/api/endpoint_test.go +++ b/consumers/notifiers/api/endpoint_test.go @@ -17,9 +17,9 @@ import ( "github.com/absmach/magistrala/consumers/notifiers" "github.com/absmach/magistrala/consumers/notifiers/api" "github.com/absmach/magistrala/consumers/notifiers/mocks" - "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/absmach/magistrala/pkg/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/consumers/notifiers/service_test.go b/consumers/notifiers/service_test.go index 27e86cdf5..ca93a56d8 100644 --- a/consumers/notifiers/service_test.go +++ b/consumers/notifiers/service_test.go @@ -12,12 +12,12 @@ import ( smqmocks "github.com/absmach/magistrala/consumers/mocks" "github.com/absmach/magistrala/consumers/notifiers" "github.com/absmach/magistrala/consumers/notifiers/mocks" - "github.com/absmach/magistrala/internal/testsutil" smqauthn "github.com/absmach/magistrala/pkg/authn" authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/absmach/magistrala/pkg/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/consumers/notifiers/smtp/notifier.go b/consumers/notifiers/smtp/notifier.go index 81b04db76..92815e48c 100644 --- a/consumers/notifiers/smtp/notifier.go +++ b/consumers/notifiers/smtp/notifier.go @@ -7,7 +7,7 @@ import ( "fmt" "github.com/absmach/magistrala/consumers" - "github.com/absmach/magistrala/internal/email" + "github.com/absmach/magistrala/pkg/email" "github.com/absmach/magistrala/pkg/messaging" ) diff --git a/consumers/writers/README.md b/consumers/writers/README.md index 47480fc9f..5059cf70d 100644 --- a/consumers/writers/README.md +++ b/consumers/writers/README.md @@ -104,7 +104,7 @@ time_fields = [ ] ``` -The topic filter uses slash-delimited MQTT-style syntax (`+`, `#`) in the config file for both backends. Writers do not expose broker mode, delivery policy, or consumer-group settings in this file. They always consume through the stream-backed broker adapter in `consumers/writers/brokers`: +The topic filter uses slash-delimited MQTT-style syntax (`+`, `#`) in the config file for both backends. Writers do not expose broker mode, delivery policy, or consumer-group settings in this file. They always consume through the stream-backed broker adapter in `pkg/messaging/writers`: - NATS builds use JetStream streams with durable consumers. - FluxMQ builds publish to and consume from the `writers` stream queue while preserving the same `writers/#` config syntax. @@ -129,7 +129,7 @@ The topic filter uses slash-delimited MQTT-style syntax (`+`, `#`) in the config ### Components -- **Message broker adapter**: `consumers/writers/brokers` (NATS JetStream or FluxMQ stream queues). +- **Message broker adapter**: `pkg/messaging/writers` (NATS JetStream or FluxMQ stream queues). - **Writer services**: `consumers/writers/postgres` and `consumers/writers/timescale`. - **HTTP API**: `consumers/writers/api` exposes `/health` and `/metrics`. - **Migrations**: `consumers/writers/*/init.go` defines the schema and indexes. diff --git a/docker/.env b/docker/.env index 55398eab6..fc64c6de8 100644 --- a/docker/.env +++ b/docker/.env @@ -280,13 +280,6 @@ MG_EMAIL_PASSWORD=password MG_EMAIL_FROM_ADDRESS=from@example.com MG_EMAIL_FROM_NAME=Example MG_EMAIL_TEMPLATE= -MG_EMAIL_INVITATION_TEMPLATE=invitation-sent-email.tmpl -MG_EMAIL_ACCEPTANCE_TEMPLATE=invitation-accepted-email.tmpl -MG_EMAIL_REJECTION_TEMPLATE=invitation-rejected-email.tmpl - -### Notifications -MG_NOTIFICATIONS_LOG_LEVEL=debug -MG_NOTIFICATIONS_INSTANCE_ID= ### Google OAuth2 MG_GOOGLE_CLIENT_ID= @@ -438,23 +431,6 @@ MG_TIMESCALE_SSL_CERT= MG_TIMESCALE_SSL_KEY= MG_TIMESCALE_SSL_ROOT_CERT= -### Journal -MG_JOURNAL_LOG_LEVEL=info -MG_JOURNAL_HTTP_HOST=journal -MG_JOURNAL_HTTP_PORT=9021 -MG_JOURNAL_HTTP_SERVER_CERT= -MG_JOURNAL_HTTP_SERVER_KEY= -MG_JOURNAL_DB_HOST=journal-db -MG_JOURNAL_DB_PORT=5432 -MG_JOURNAL_DB_USER=magistrala -MG_JOURNAL_DB_PASS=magistrala -MG_JOURNAL_DB_NAME=journal -MG_JOURNAL_DB_SSL_MODE=disable -MG_JOURNAL_DB_SSL_CERT= -MG_JOURNAL_DB_SSL_KEY= -MG_JOURNAL_DB_SSL_ROOT_CERT= -MG_JOURNAL_INSTANCE_ID= - ### GRAFANA and PROMETHEUS MG_PROMETHEUS_PORT=9090 MG_GRAFANA_PORT=3001 @@ -671,8 +647,6 @@ MG_UI_BACKEND_INSTANCE_ID= MG_UI_BACKEND_URL=http://ui-backend:9097 MG_UI_VERIFICATION_TLS=false MG_UI_CONTENT_TYPE=application/senml+json -# Set to yes to accept the EULA for the UI services. To view the EULA visit: https://github.com/absmach/eula -MG_UI_DOCKER_ACCEPT_EULA=yes OTEL_SERVICE_NAME=ui-mg OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 @@ -711,7 +685,6 @@ MG_BOOTSTRAP_URL=http://bootstrap:9013 MG_HTTP_ADAPTER_URL=http://nginx:80/http MG_PUBLISH_PROXY_URL=http://nginx:80 MG_READER_URL=http://timescale-reader:9011 -MG_JOURNAL_URL=http://journal:9021 ### UI Configuration MG_UI_TYPE=mg diff --git a/docker/README.md b/docker/README.md index b3dd52dfb..812c127f9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -180,7 +180,7 @@ For a trusted production certificate, set `staging=false`: make run_tls host=example.com email=admin@example.com staging=false ``` -The target updates `docker/.env`, starts the Compose stack with the fallback certificate, runs certbot, switches Nginx to the issued certificate, and recreates Nginx. It also sets `MG_UI_DOCKER_ACCEPT_EULA=yes` for the UI container and configures public UI URLs to `https://${MG_PUBLIC_HOST}`. +The target updates `docker/.env`, starts the Compose stack with the fallback certificate, runs certbot, switches Nginx to the issued certificate, and recreates Nginx. It also configures public UI URLs to `https://${MG_PUBLIC_HOST}`. To configure the same instance without Let's Encrypt, use: diff --git a/docker/addons/bootstrap/docker-compose.yaml b/docker/addons/bootstrap/docker-compose.yaml index c96902f31..b48059ae1 100644 --- a/docker/addons/bootstrap/docker-compose.yaml +++ b/docker/addons/bootstrap/docker-compose.yaml @@ -15,7 +15,7 @@ volumes: services: bootstrap-db: - image: postgres:16.2-alpine + image: postgres:16.14-alpine3.24 container_name: magistrala-bootstrap-db restart: on-failure environment: diff --git a/docker/addons/postgres-writer/docker-compose.yaml b/docker/addons/postgres-writer/docker-compose.yaml index 1d3765873..023867af1 100644 --- a/docker/addons/postgres-writer/docker-compose.yaml +++ b/docker/addons/postgres-writer/docker-compose.yaml @@ -17,7 +17,7 @@ volumes: services: postgres: - image: postgres:16.2-alpine + image: postgres:16.14-alpine3.24 container_name: magistrala-postgres restart: on-failure environment: diff --git a/docker/addons/prometheus/docker-compose.yaml b/docker/addons/prometheus/docker-compose.yaml index 06543f359..c38d5e745 100644 --- a/docker/addons/prometheus/docker-compose.yaml +++ b/docker/addons/prometheus/docker-compose.yaml @@ -18,7 +18,7 @@ volumes: services: promethues: - image: prom/prometheus:v2.49.1 + image: prom/prometheus:v3.13.1 container_name: magistrala-prometheus restart: on-failure ports: @@ -32,7 +32,7 @@ services: - magistrala-prometheus-volume:/prometheus grafana: - image: grafana/grafana:10.2.3 + image: grafana/grafana:13.1.1 container_name: magistrala-grafana depends_on: - promethues diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8845d11e9..01845fdd4 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -14,7 +14,6 @@ networks: volumes: magistrala-pat-db-volume: magistrala-ui-backend-db-volume: - magistrala-journal-volume: magistrala-re-db-volume: magistrala-alarms-db-volume: magistrala-reports-db-volume: @@ -128,65 +127,8 @@ services: networks: - magistrala-base-net - journal-db: - image: postgres:16.2-alpine - container_name: magistrala-journal-db - restart: on-failure - command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" - environment: - POSTGRES_USER: ${MG_JOURNAL_DB_USER} - POSTGRES_PASSWORD: ${MG_JOURNAL_DB_PASS} - POSTGRES_DB: ${MG_JOURNAL_DB_NAME} - MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} - networks: - - magistrala-base-net - volumes: - - magistrala-journal-volume:/var/lib/postgresql/data - - journal: - image: ghcr.io/absmach/magistrala/journal:${MG_RELEASE_TAG} - container_name: magistrala-journal - depends_on: - journal-db: - condition: service_started - atom-bootstrap: - condition: service_completed_successfully - nginx: - condition: service_started - restart: on-failure - environment: - MG_JOURNAL_LOG_LEVEL: ${MG_JOURNAL_LOG_LEVEL} - MG_JOURNAL_HTTP_HOST: ${MG_JOURNAL_HTTP_HOST} - MG_JOURNAL_HTTP_PORT: ${MG_JOURNAL_HTTP_PORT} - MG_JOURNAL_HTTP_SERVER_CERT: ${MG_JOURNAL_HTTP_SERVER_CERT} - MG_JOURNAL_HTTP_SERVER_KEY: ${MG_JOURNAL_HTTP_SERVER_KEY} - MG_JOURNAL_DB_HOST: ${MG_JOURNAL_DB_HOST} - MG_JOURNAL_DB_PORT: ${MG_JOURNAL_DB_PORT} - MG_JOURNAL_DB_USER: ${MG_JOURNAL_DB_USER} - MG_JOURNAL_DB_PASS: ${MG_JOURNAL_DB_PASS} - MG_JOURNAL_DB_NAME: ${MG_JOURNAL_DB_NAME} - MG_JOURNAL_DB_SSL_MODE: ${MG_JOURNAL_DB_SSL_MODE} - MG_JOURNAL_DB_SSL_CERT: ${MG_JOURNAL_DB_SSL_CERT} - MG_JOURNAL_DB_SSL_KEY: ${MG_JOURNAL_DB_SSL_KEY} - MG_JOURNAL_DB_SSL_ROOT_CERT: ${MG_JOURNAL_DB_SSL_ROOT_CERT} - ATOM_URL: ${ATOM_URL} - ATOM_SERVICE_TOKEN: ${MG_ATOM_TOKEN_JOURNAL} - ATOM_JWKS_URL: ${ATOM_JWKS_URL} - ATOM_JWT_ISSUER: ${ATOM_JWT_ISSUER} - ATOM_JWT_AUDIENCE: ${ATOM_JWT_AUDIENCE} - ATOM_TIMEOUT: ${ATOM_TIMEOUT} - MG_ES_URL: ${MG_ES_URL} - MG_JAEGER_URL: ${MG_JAEGER_URL} - MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} - MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} - MG_JOURNAL_INSTANCE_ID: ${MG_JOURNAL_INSTANCE_ID} - MG_ALLOW_UNVERIFIED_USER: ${MG_ALLOW_UNVERIFIED_USER} - ports: - - ${MG_JOURNAL_HTTP_PORT}:${MG_JOURNAL_HTTP_PORT} - networks: - - magistrala-base-net nginx: - image: docker.io/nginx:1.29.2-alpine3.22 + image: docker.io/nginx:1.31.3-alpine3.24 container_name: magistrala-nginx restart: on-failure volumes: @@ -228,7 +170,7 @@ services: hard: 65536 certbot: - image: docker.io/certbot/certbot:v2.11.0 + image: docker.io/certbot/certbot:v5.7.0 container_name: magistrala-certbot profiles: - letsencrypt @@ -284,46 +226,8 @@ services: sleep 12h & wait $$! done - notifications: - image: ghcr.io/absmach/magistrala/notifications:${MG_RELEASE_TAG} - container_name: magistrala-notifications - depends_on: - atom-bootstrap: - condition: service_completed_successfully - nginx: - condition: service_started - restart: on-failure - environment: - MG_NOTIFICATIONS_LOG_LEVEL: ${MG_NOTIFICATIONS_LOG_LEVEL} - MG_NOTIFICATIONS_INSTANCE_ID: ${MG_NOTIFICATIONS_INSTANCE_ID} - MG_ES_URL: ${MG_ES_URL} - MG_JAEGER_URL: ${MG_JAEGER_URL} - MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} - MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} - MG_EMAIL_HOST: ${MG_EMAIL_HOST} - MG_EMAIL_PORT: ${MG_EMAIL_PORT} - MG_EMAIL_USERNAME: ${MG_EMAIL_USERNAME} - MG_EMAIL_PASSWORD: ${MG_EMAIL_PASSWORD} - MG_EMAIL_FROM_ADDRESS: ${MG_EMAIL_FROM_ADDRESS} - MG_EMAIL_FROM_NAME: ${MG_EMAIL_FROM_NAME} - MG_EMAIL_INVITATION_TEMPLATE: ${MG_EMAIL_INVITATION_TEMPLATE} - MG_EMAIL_ACCEPTANCE_TEMPLATE: ${MG_EMAIL_ACCEPTANCE_TEMPLATE} - MG_EMAIL_REJECTION_TEMPLATE: ${MG_EMAIL_REJECTION_TEMPLATE} - ATOM_URL: ${ATOM_URL} - ATOM_SERVICE_TOKEN: ${MG_ATOM_TOKEN_NOTIFICATIONS} - ATOM_JWKS_URL: ${ATOM_JWKS_URL} - ATOM_JWT_ISSUER: ${ATOM_JWT_ISSUER} - ATOM_JWT_AUDIENCE: ${ATOM_JWT_AUDIENCE} - ATOM_TIMEOUT: ${ATOM_TIMEOUT} - networks: - - magistrala-base-net - volumes: - - ./templates/${MG_EMAIL_INVITATION_TEMPLATE}:/${MG_EMAIL_INVITATION_TEMPLATE} - - ./templates/${MG_EMAIL_ACCEPTANCE_TEMPLATE}:/${MG_EMAIL_ACCEPTANCE_TEMPLATE} - - ./templates/${MG_EMAIL_REJECTION_TEMPLATE}:/${MG_EMAIL_REJECTION_TEMPLATE} - jaeger: - image: docker.io/jaegertracing/all-in-one:1.74.0 + image: docker.io/jaegertracing/all-in-one:1.76.0 container_name: magistrala-jaeger environment: COLLECTOR_OTLP_ENABLED: ${MG_JAEGER_COLLECTOR_OTLP_ENABLED} @@ -449,7 +353,6 @@ services: MG_PUBLISH_PROXY_URL: ${MG_PUBLISH_PROXY_URL} MG_READER_URL: ${MG_READER_URL} MG_BACKEND_URL: ${MG_UI_BACKEND_URL} - MG_JOURNAL_URL: ${MG_JOURNAL_URL} MG_ALARMS_URL: ${MG_ALARMS_URL} MG_RE_URL: ${MG_RE_URL} MG_REPORTS_URL: ${MG_REPORTS_URL} @@ -467,7 +370,6 @@ services: NEXT_LOG_LEVEL: "debug" MG_HOST_URL: ${MG_HOST_URL} MG_UI_IMAGE_URL: ${MG_UI_IMAGE_URL} - MG_UI_DOCKER_ACCEPT_EULA: ${MG_UI_DOCKER_ACCEPT_EULA} MG_SUPPORT_EMAIL: ${MG_SUPPORT_EMAIL} MG_SUPPORT_EMAIL_PASS: ${MG_SUPPORT_EMAIL_PASS} MG_UI_CLI_MQTT_HOST: ${MG_UI_CLI_MQTT_HOST} @@ -521,7 +423,6 @@ services: MG_UI_VERIFICATION_TLS: ${MG_UI_VERIFICATION_TLS} MG_UI_CONTENT_TYPE: ${MG_UI_CONTENT_TYPE} MG_READER_URL: ${MG_READER_URL} - MG_UI_DOCKER_ACCEPT_EULA: ${MG_UI_DOCKER_ACCEPT_EULA} MG_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} @@ -601,11 +502,12 @@ services: create_host_path: true ui-backend-db: - image: docker.io/postgres:18.0-alpine3.22 + image: docker.io/postgres:18.4-alpine3.24 container_name: magistrala-ui-backend-db restart: on-failure command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" environment: + PGDATA: /var/lib/postgresql/data POSTGRES_USER: ${MG_UI_BACKEND_DB_USER} POSTGRES_PASSWORD: ${MG_UI_BACKEND_DB_PASS} POSTGRES_DB: ${MG_UI_BACKEND_DB_NAME} @@ -623,7 +525,7 @@ services: retries: 60 seaweedfs-s3: - image: chrislusf/seaweedfs:4.16 + image: chrislusf/seaweedfs:4.40 container_name: magistrala-seaweedfs-s3 command: server -s3 -s3.config=/etc/seaweedfs/s3.json -dir=/data ports: @@ -668,7 +570,7 @@ services: AWS_EC2_METADATA_DISABLED: "true" timescale: - image: timescale/timescaledb:2.19.3-pg16-oss + image: timescale/timescaledb:2.29.0-pg16-oss container_name: magistrala-timescale restart: on-failure environment: @@ -796,11 +698,12 @@ services: volumes: - ./addons/timescale-writer/config.toml:${MG_TIMESCALE_WRITER_CONFIG_PATH} re-db: - image: docker.io/postgres:18.0-alpine3.22 + image: docker.io/postgres:18.4-alpine3.24 container_name: magistrala-re-db restart: on-failure command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" environment: + PGDATA: /var/lib/postgresql/data POSTGRES_USER: ${MG_RE_DB_USER} POSTGRES_PASSWORD: ${MG_RE_DB_PASS} POSTGRES_DB: ${MG_RE_DB_NAME} @@ -880,11 +783,12 @@ services: - ./templates/${MG_RE_EMAIL_TEMPLATE}:/email.tmpl alarms-db: - image: docker.io/postgres:18.0-alpine3.22 + image: docker.io/postgres:18.4-alpine3.24 container_name: magistrala-alarms-db restart: on-failure command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" environment: + PGDATA: /var/lib/postgresql/data POSTGRES_USER: ${MG_ALARMS_DB_USER} POSTGRES_PASSWORD: ${MG_ALARMS_DB_PASS} POSTGRES_DB: ${MG_ALARMS_DB_NAME} @@ -943,11 +847,12 @@ services: - ./permission.yaml:${MG_PERMISSIONS_FILE} reports-db: - image: docker.io/postgres:18.0-alpine3.22 + image: docker.io/postgres:18.4-alpine3.24 container_name: magistrala-reports-db restart: on-failure command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" environment: + PGDATA: /var/lib/postgresql/data POSTGRES_USER: ${MG_REPORTS_DB_USER} POSTGRES_PASSWORD: ${MG_REPORTS_DB_PASS} POSTGRES_DB: ${MG_REPORTS_DB_NAME} diff --git a/docker/fluxmq/node1.yaml b/docker/fluxmq/node1.yaml index 729906e83..ad1feb09b 100644 --- a/docker/fluxmq/node1.yaml +++ b/docker/fluxmq/node1.yaml @@ -127,15 +127,16 @@ queues: max_length_bytes: 1073741824 auth: - url: "http://fluxmq-auth:7016" - transport: "grpc" - timeout: 15s - protocols: - mqtt: true - http: true - coap: true - amqp: true - amqp091: false + external: + url: "http://fluxmq-auth:7016" + transport: "grpc" + timeout: 15s + protocols: + mqtt: true + http: true + coap: true + amqp: true + amqp091: false hooks: url: "http://fluxmq-auth:7016" diff --git a/docker/fluxmq/node2.yaml b/docker/fluxmq/node2.yaml index a3dfd5df9..e7bec000f 100644 --- a/docker/fluxmq/node2.yaml +++ b/docker/fluxmq/node2.yaml @@ -124,15 +124,16 @@ queues: max_length_bytes: 1073741824 auth: - url: "http://fluxmq-auth:7016" - transport: "grpc" - timeout: 15s - protocols: - mqtt: true - http: true - coap: true - amqp: true - amqp091: false + external: + url: "http://fluxmq-auth:7016" + transport: "grpc" + timeout: 15s + protocols: + mqtt: true + http: true + coap: true + amqp: true + amqp091: false hooks: url: "http://fluxmq-auth:7016" diff --git a/docker/fluxmq/node3.yaml b/docker/fluxmq/node3.yaml index 260e0702b..f4801f871 100644 --- a/docker/fluxmq/node3.yaml +++ b/docker/fluxmq/node3.yaml @@ -124,15 +124,16 @@ queues: max_length_bytes: 1073741824 auth: - url: "http://fluxmq-auth:7016" - transport: "grpc" - timeout: 15s - protocols: - mqtt: true - http: true - coap: true - amqp: true - amqp091: false + external: + url: "http://fluxmq-auth:7016" + transport: "grpc" + timeout: 15s + protocols: + mqtt: true + http: true + coap: true + amqp: true + amqp091: false hooks: url: "http://fluxmq-auth:7016" diff --git a/docker/setup-tls.sh b/docker/setup-tls.sh index 150479ee5..f7ad145cf 100755 --- a/docker/setup-tls.sh +++ b/docker/setup-tls.sh @@ -213,7 +213,6 @@ set_env MG_LETSENCRYPT_FORCE_RENEWAL "$FORCE_RENEWAL" set_env MG_NGINX_SERVER_NAME "$HOST" comment_env_any MG_NGINX_SERVER_CERT comment_env_any MG_NGINX_SERVER_KEY -set_env MG_UI_DOCKER_ACCEPT_EULA yes set_env MG_OAUTH_UI_REDIRECT_URL "https://$HOST/api/auth/token" set_env MG_OAUTH_UI_ERROR_URL "https://$HOST/login" @@ -234,19 +233,19 @@ write_ui_proxy if [ "$LETSENCRYPT_ENABLED" = "false" ]; then echo "Starting Magistrala with the fallback Nginx certificate" - MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d - MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose stop certbot >/dev/null 2>&1 || true + compose up -d + COMPOSE_PROFILES=letsencrypt compose stop certbot >/dev/null 2>&1 || true echo "Let's Encrypt disabled. Nginx cert/key paths are commented in docker/.env." echo "Fallback TLS setup complete: https://$HOST/" exit 0 fi echo "Starting Magistrala with the fallback Nginx certificate" -MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d +compose up -d wait_for_nginx_http echo "Requesting Let's Encrypt certificate for $HOST" -MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose up -d --force-recreate certbot +COMPOSE_PROFILES=letsencrypt compose up -d --force-recreate certbot cert_ready() { compose logs certbot 2>&1 | \ @@ -271,6 +270,6 @@ set_env MG_NGINX_SERVER_CERT "$cert_path" set_env MG_NGINX_SERVER_KEY "$key_path" set_env MG_LETSENCRYPT_FORCE_RENEWAL false -MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d --force-recreate nginx +compose up -d --force-recreate nginx echo "TLS setup complete: https://$HOST/" diff --git a/docker/templates/invitation-accepted-email.tmpl b/docker/templates/invitation-accepted-email.tmpl deleted file mode 100644 index 3aaabf29a..000000000 --- a/docker/templates/invitation-accepted-email.tmpl +++ /dev/null @@ -1,100 +0,0 @@ - - - - - {{.Subject}} - - - -
-
-

Invitation Accepted

-
- -
-
✓
- -

Dear {{.User}},

- -
-

{{.Content}}

-
- -

You can now collaborate with them in the domain.

- - - -

Best regards,
{{.Footer}}

-
- - -
- - diff --git a/docker/templates/invitation-rejected-email.tmpl b/docker/templates/invitation-rejected-email.tmpl deleted file mode 100644 index 42c336e58..000000000 --- a/docker/templates/invitation-rejected-email.tmpl +++ /dev/null @@ -1,93 +0,0 @@ - - - - - {{.Subject}} - - - -
-
-

Invitation Declined

-
- -
-

Dear {{.User}},

- -
-

{{.Content}}

-
- -

If you would like to extend another invitation or have any questions, please don't hesitate to contact us.

- - - -

Best regards,
{{.Footer}}

-
- - -
- - diff --git a/docker/templates/invitation-sent-email.tmpl b/docker/templates/invitation-sent-email.tmpl deleted file mode 100644 index 7d13d3423..000000000 --- a/docker/templates/invitation-sent-email.tmpl +++ /dev/null @@ -1,106 +0,0 @@ - - - - - {{.Subject}} - - - -
-
-

Domain Invitation

-
- -
-

Dear {{.User}},

- -
-

{{.Content}}

-
- -

Please log in to your account to view and respond to this invitation.

- -

If you have any questions, please don't hesitate to contact us.

- - - -

Best regards,
{{.Footer}}

-
- - -
- - diff --git a/fluxmq/api/grpc/server.go b/fluxmq/api/grpc/server.go index 6b223e8af..f144e3534 100644 --- a/fluxmq/api/grpc/server.go +++ b/fluxmq/api/grpc/server.go @@ -13,7 +13,7 @@ import ( grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1" grpcClientsV1 "github.com/absmach/magistrala/api/grpc/clients/v1" apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/connections" "github.com/absmach/magistrala/pkg/errors" diff --git a/fluxmq/api/grpc/server_test.go b/fluxmq/api/grpc/server_test.go index ad696aa82..53a913b55 100644 --- a/fluxmq/api/grpc/server_test.go +++ b/fluxmq/api/grpc/server_test.go @@ -9,7 +9,7 @@ import ( "connectrpc.com/connect" authv1 "github.com/absmach/fluxmq/pkg/proto/auth/v1" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/messaging" "github.com/stretchr/testify/require" ) diff --git a/fluxmq/api/http/publish.go b/fluxmq/api/http/publish.go index 888897a53..cef62a469 100644 --- a/fluxmq/api/http/publish.go +++ b/fluxmq/api/http/publish.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" smqauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/messaging" "github.com/go-chi/chi/v5" diff --git a/go.mod b/go.mod index bdc93628e..dde6fedfa 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f github.com/ivanpirog/coloredcobra v1.0.1 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 - github.com/jackc/pgtype v1.14.4 github.com/jackc/pgx/v5 v5.10.0 github.com/jmoiron/sqlx v1.4.0 github.com/lestrrat-go/jwx/v2 v2.1.7 @@ -37,16 +36,13 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/plgd-dev/go-coap/v3 v3.5.4 github.com/prometheus/client_golang v1.24.1 + github.com/rabbitmq/amqp091-go v1.13.0 github.com/redis/go-redis/v9 v9.21.0 github.com/rubenv/sql-migrate v1.8.1 - github.com/slack-go/slack v0.27.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/sqids/sqids-go v0.4.1 github.com/stretchr/testify v1.11.1 - github.com/traefik/yaegi v0.16.1 - github.com/vadv/gopher-lua-libs v0.8.0 - github.com/yuin/gopher-lua v1.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 go.opentelemetry.io/otel v1.44.0 @@ -65,15 +61,8 @@ require ( moul.io/http2curl v1.0.0 ) -require ( - github.com/smarty/assertions v1.16.0 // indirect - github.com/smartystreets/goconvey v1.8.1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect -) - require ( dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect @@ -115,7 +104,6 @@ require ( github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -147,11 +135,12 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect - github.com/rabbitmq/amqp091-go v1.13.0 github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/smarty/assertions v1.16.0 // indirect + github.com/smartystreets/goconvey v1.8.1 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -172,6 +161,6 @@ require ( golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0b6d04cfb..b85f4a5d5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= @@ -14,12 +12,10 @@ github.com/0x6flab/namegenerator v1.4.0/go.mod h1:2sQzXuS6dX/KEwWtB6GJU729O3m4gB github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/absmach/callhome v0.18.2 h1:dmopRHm2qTheHN1hdUKRRYpKwRrj7X9d8AWCFrb+K6s= @@ -28,16 +24,6 @@ github.com/absmach/fluxmq v0.40.0 h1:J7s6PHXliWRfwpQpt/umRNnrUegj9xYsVyK9BV8Azhk github.com/absmach/fluxmq v0.40.0/go.mod h1:oVbq3VlkD0vPKc45gkcxXpD2tbweyF+CDw1x72AS+PA= github.com/absmach/senml v1.0.8 h1:+opem/r4g6c6eA/JLyCIuksyEhj7eBdysY3pEmy1mqo= github.com/absmach/senml v1.0.8/go.mod h1:DRhzHLgvQoIUHroBgpFrSWso+bJZO9E96RlHAHy+VRI= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI= -github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -48,32 +34,21 @@ github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdz github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18= github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw= github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= -github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw= -github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cheggaaa/pb/v3 v3.0.5/go.mod h1:X1L61/+36nz9bjIsrDU52qHKOQukUQe2Ge+YvGuquCw= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -96,12 +71,10 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/golib/memfile v1.0.0 h1:J9pUspY2bDCbF9o+YGwcf3uG6MdyITfh/Fk3/CaEiFs= github.com/dsnet/golib/memfile v1.0.0/go.mod h1:tXGNW9q3RwvWt1VV2qrRKlSSz0npnh12yftCSCy2T64= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= @@ -121,16 +94,10 @@ github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -138,42 +105,21 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/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-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -214,91 +160,26 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLfbgNxrN4= github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -315,33 +196,19 @@ github.com/lestrrat-go/jwx/v2 v2.1.7 h1:bnYeET+S8IOyAw6W4LTc6SEeK7Xs58SKKZkR7scb github.com/lestrrat-go/jwx/v2 v2.1.7/go.mod h1:exQ9ZBuN1cMLYmxwhTlHUru08ykONG0z+HbLEeDG9qo= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -354,15 +221,8 @@ github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= @@ -392,9 +252,6 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/plgd-dev/go-coap/v3 v3.5.4 h1:KtSx2upgpeMzjdYXsEZDgUYui9l08HfoO6DldCI7Msc= github.com/plgd-dev/go-coap/v3 v3.5.4/go.mod h1:kgdxil4mi3Bi9s5av/NbQeVwRJ+8N6zGHFEPy7qTRWI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -402,38 +259,20 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rabbitmq/amqp091-go v1.13.0 h1:L8NA1WtF76C6KA3LAoufjfLgbist/If1UQYcsOjtxXA= github.com/rabbitmq/amqp091-go v1.13.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -442,20 +281,11 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/slack-go/slack v0.27.0 h1:VWOpUzOK6UAPCCQlFxl79jhv8a/b+GOSJMnWziDJ8B8= -github.com/slack-go/slack v0.27.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4= github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY= github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -476,32 +306,17 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw= github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= -github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= -github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= -github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vadv/gopher-lua-libs v0.8.0 h1:u2GVTj32Wnmu8RpSxeAdlTf9mYZrrm9ALKGYmvnvvZQ= -github.com/vadv/gopher-lua-libs v0.8.0/go.mod h1:iNYvPoNV6ur7xJj4Uj3hEVebv8Z0/MoeM1igsXQbv8g= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -513,15 +328,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs= -github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= -github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= @@ -544,186 +352,59 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw= google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/xmlpath.v2 v2.0.0-20150820204837-860cbeca3ebc h1:LMEBgNcZUqXaP7evD1PZcL6EcDVa2QOFuI+cqM3+AJM= -gopkg.in/xmlpath.v2 v2.0.0-20150820204837-860cbeca3ebc/go.mod h1:N8UOSI6/c2yOpa/XDz3KVUiegocTziPiqNkeNTMiG1k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= diff --git a/journal/README.md b/journal/README.md deleted file mode 100644 index 96f83100a..000000000 --- a/journal/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Journal - -The Journal service listens to the platform event stream, persists each event to PostgreSQL for auditability and exposes HTTP endpoints to query journals or view per-client telemetry (first/last seen, subscriptions, in/out message counters). - -## Configuration - -The service is configured with the following environment variables (unset values fall back to defaults). - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_JOURNAL_LOG_LEVEL` | Log level for Journal (debug, info, warn, error) | info | -| `MG_JOURNAL_HTTP_HOST` | Journal HTTP host | localhost | -| `MG_JOURNAL_HTTP_PORT` | Journal HTTP port | 9021 | -| `MG_JOURNAL_HTTP_SERVER_CERT` | Path to PEM-encoded HTTP server certificate | "" | -| `MG_JOURNAL_HTTP_SERVER_KEY` | Path to PEM-encoded HTTP server key | "" | -| `MG_JOURNAL_HTTP_SERVER_CA_CERTS` | Path to trusted CA bundle for the HTTP server | "" | -| `MG_JOURNAL_HTTP_CLIENT_CA_CERTS` | Path to client CA bundle to require HTTP mTLS | "" | -| `MG_JOURNAL_DB_HOST` | Database host address | localhost | -| `MG_JOURNAL_DB_PORT` | Database host port | 5432 | -| `MG_JOURNAL_DB_USER` | Database user | magistrala | -| `MG_JOURNAL_DB_PASS` | Database password | magistrala | -| `MG_JOURNAL_DB_NAME` | Name of the database used by the service | journal | -| `MG_JOURNAL_DB_SSL_MODE` | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable | -| `MG_JOURNAL_DB_SSL_CERT` | Path to the PEM-encoded certificate file | "" | -| `MG_JOURNAL_DB_SSL_KEY` | Path to the PEM-encoded key file | "" | -| `MG_JOURNAL_DB_SSL_ROOT_CERT` | Path to the PEM-encoded root certificate file | "" | -| `MG_ES_URL` | Event store URL (NATS) consumed for journal entries | nats://localhost:4222 | -| `MG_JAEGER_URL` | Jaeger tracing endpoint | | -| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | 1.0 | -| `MG_SEND_TELEMETRY` | Send telemetry to the Magistrala call-home server | true | -| `MG_AUTH_GRPC_URL` | Auth service gRPC URL | "" | -| `MG_AUTH_GRPC_TIMEOUT` | Auth service gRPC timeout | 1s | -| `MG_AUTH_GRPC_CLIENT_CERT` | Path to PEM-encoded Auth gRPC client certificate | "" | -| `MG_AUTH_GRPC_CLIENT_KEY` | Path to PEM-encoded Auth gRPC client key | "" | -| `MG_AUTH_GRPC_SERVER_CA_CERTS` | Path to PEM-encoded Auth gRPC trusted CA bundle | "" | -| `MG_DOMAINS_GRPC_URL` | Domains service gRPC URL | "" | -| `MG_DOMAINS_GRPC_TIMEOUT` | Domains service gRPC timeout | 1s | -| `MG_DOMAINS_GRPC_CLIENT_CERT` | Path to PEM-encoded Domains gRPC client certificate | "" | -| `MG_DOMAINS_GRPC_CLIENT_KEY` | Path to PEM-encoded Domains gRPC client key | "" | -| `MG_DOMAINS_GRPC_SERVER_CA_CERTS` | Path to PEM-encoded Domains gRPC trusted CA bundle | "" | -| `MG_JOURNAL_INSTANCE_ID` | Journal instance ID (auto-generated when empty) | "" | -| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to authenticate (useful in dev) | false | - -## Deployment - -The service is distributed as a Docker container. Check [`docker/docker-compose.yaml`](https://github.com/absmach/magistrala/tree/main/docker/docker-compose.yaml) for the `journal` and `journal-db` services and how they are wired into the base stack. - -To start the service outside of the container, execute the following shell script: - -```bash -git clone https://github.com/absmach/magistrala -cd magistrala - -# build and install the binary -make journal -make install - -# run with the essentials; requires Postgres, Auth gRPC, Domains gRPC, and NATS running -MG_JOURNAL_HTTP_HOST=localhost \ -MG_JOURNAL_HTTP_PORT=9021 \ -MG_JOURNAL_DB_HOST=localhost \ -MG_JOURNAL_DB_PORT=5432 \ -MG_JOURNAL_DB_USER=magistrala \MG_JOURNAL_DB_PASS=magistrala \MG_JOURNAL_DB_NAME=journal \ -MG_AUTH_GRPC_URL=localhost:7001 \ -MG_DOMAINS_GRPC_URL=localhost:7003 \ -MG_ES_URL=nats://localhost:4222 \ -$GOBIN/magistrala-journal -``` - -## HTTP API - -Base URL defaults to `http://localhost:9021`. All journal and telemetry endpoints require `Authorization: Bearer ` (health is public). - -### Usage - -| Operation | Description | -| --- | --- | -| List user journals | Page through journals for a user across domains. | -| List entity journals | Page through journals for a group, client, channel, or user within a domain. | -| View client telemetry | Aggregate telemetry counters for a client in a domain. | -| Health check | Liveness and build info. | - -### API examples - -#### List user journals - -```bash -curl -X GET "http://localhost:9021/journal/user/${USER_ID}?limit=5&with_attributes=true&dir=desc" \ - -H "Authorization: Bearer $TOKEN" -``` - -Expected response: - -```json -{ - "journals": [ - { - "operation": "user.create", - "occurred_at": "2024-01-11T12:05:07.449053Z", - "attributes": { - "created_at": "2024-06-12T11:34:32.991591Z", - "id": "29d425c8-542b-4614-8a4d-a5951945d720", - "identity": "Gawne-Havlicek@email.com", - "name": "Newgard-Frisina", - "status": "enabled", - "updated_at": "2024-06-12T11:34:33.116795Z", - "updated_by": "ad228f20-4741-47c5-bef7-d871b541c019" - }, - "metadata": { - "Update": "Calvo-Felkins" - } - } - ], - "total": 1, - "offset": 0, - "limit": 5 -} -``` - -#### List entity journals in a domain - -Retrieves telemetry data for a specific client within a domain. This includes connection status, messages sent/received, and other metrics. - -```bash -curl -X GET "http://localhost:9021/${DOMAIN_ID}/journal/client/${CLIENT_ID}?operation=client.create&with_metadata=true&dir=desc&limit=10" \ - -H "Authorization: Bearer $TOKEN" -``` - -Expected response: - -```json -{ - "total": 2, - "offset": 0, - "limit": 10, - "journals": [ - { - "operation": "client.create", - "occurred_at": "2024-06-12T11:34:33Z", - "domain": "29d425c8-542b-4614-8a4d-a5951945d720", - "attributes": { - "id": "bb7edb32-2eac-4aad-aebe-ed96fe073879", - "domain": "29d425c8-542b-4614-8a4d-a5951945d720", - "name": "clientName", - "status": "enabled" - }, - "metadata": { - "trace_id": "6efb4c24b1b4a684" - } - } - ] -} -``` - -#### View client telemetry - -Retrieves telemetry data for a specific client within a domain. This includes connection status, messages sent/received, and other metrics. - -```bash -curl -X GET "http://localhost:9021/${DOMAIN_ID}/journal/client/${CLIENT_ID}/telemetry" \ - -H "Authorization: Bearer $TOKEN" -``` - -Expected response: - -```json -{ - "client_id": "bb7edb32-2eac-4aad-aebe-ed96fe073879", - "domain_id": "29d425c8-542b-4614-8a4d-a5951945d720", - "subscriptions": 5, - "inbound_messages": 1234567, - "outbound_messages": 987654, - "first_seen": "2024-01-11T10:00:00Z", - "last_seen": "2024-01-11T12:05:07.449053Z" -} -``` - -#### Health check - -```bash -curl "http://localhost:9021/health" -``` - -Expected response: - -```json -{ - "status": "pass", - "version": "0.18.0", - "commit": "ffffffff", - "description": "journal service", - "build_time": "1970-01-01_00:00:00", - "instance_id": "b4f1d5d2-4f24-4c2a-9a40-123456789abc" -} -``` diff --git a/journal/api/doc.go b/journal/api/doc.go deleted file mode 100644 index 2424852cc..000000000 --- a/journal/api/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package api contains API-related concerns: endpoint definitions, middlewares -// and all resource representations. -package api diff --git a/journal/api/endpoint.go b/journal/api/endpoint.go deleted file mode 100644 index 381bb2e38..000000000 --- a/journal/api/endpoint.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/go-kit/kit/endpoint" -) - -func retrieveJournalsEndpoint(svc journal.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(retrieveJournalsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - page, err := svc.RetrieveAll(ctx, session, req.page) - if err != nil { - return nil, err - } - - return pageRes{ - JournalsPage: page, - }, nil - } -} - -func retrieveClientTelemetryEndpoint(svc journal.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(retrieveClientTelemetryReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - telemetry, err := svc.RetrieveClientTelemetry(ctx, session, req.clientID) - if err != nil { - return nil, err - } - - return clientTelemetryRes{ - ClientTelemetry: telemetry, - }, nil - } -} diff --git a/journal/api/endpoint_test.go b/journal/api/endpoint_test.go deleted file mode 100644 index fa7980046..000000000 --- a/journal/api/endpoint_test.go +++ /dev/null @@ -1,573 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api_test - -import ( - "fmt" - "io" - "net/http" - "net/http/httptest" - "strconv" - "testing" - "time" - - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/journal" - httpapi "github.com/absmach/magistrala/journal/api" - "github.com/absmach/magistrala/journal/mocks" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - validToken = "valid" - validID = testsutil.GenerateUUID(&testing.T{}) -) - -type testRequest struct { - client *http.Client - method string - url string - token string - body io.Reader -} - -func (tr testRequest) make() (*http.Response, error) { - req, err := http.NewRequest(tr.method, tr.url, tr.body) - if err != nil { - return nil, err - } - - if tr.token != "" { - req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token) - } - - return tr.client.Do(req) -} - -func newjournalServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { - svc := new(mocks.Service) - - logger := mglog.NewMock() - authn := new(authnmocks.Authentication) - am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true)) - mux := httpapi.MakeHandler(svc, am, logger, "journal-log", "test") - return httptest.NewServer(mux), svc, authn -} - -func TestListUserJournalsEndpoint(t *testing.T) { - es, svc, authn := newjournalServer() - - cases := []struct { - desc string - token string - session smqauthn.Session - url string - contentType string - status int - svcErr error - }{ - { - desc: "successful", - token: validToken, - url: "/user/123", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "empty token", - token: "", - url: "/user/123", - status: http.StatusUnauthorized, - svcErr: nil, - }, - { - desc: "with service error", - token: validToken, - url: "/user/123", - status: http.StatusForbidden, - svcErr: svcerr.ErrAuthorization, - }, - { - desc: "with offset", - token: validToken, - url: "/user/123?offset=10", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid offset", - token: validToken, - url: "/user/123?offset=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with limit", - token: validToken, - url: "/user/123?limit=10", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid limit", - token: validToken, - url: "/user/123?limit=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with operation", - token: validToken, - url: "/user/123?operation=user.create", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with malformed operation", - token: validToken, - url: "/user/123?operation=user.create&operation=user.update", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with from", - token: validToken, - url: fmt.Sprintf("/user/123?from=%d", time.Now().Unix()), - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid from", - token: validToken, - url: "/user/123?from=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with invalid from as UnixNano", - token: validToken, - url: fmt.Sprintf("/user/123?from=%d", time.Now().UnixNano()), - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with to", - token: validToken, - url: fmt.Sprintf("/user/123?to=%d", time.Now().Unix()), - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid to", - token: validToken, - url: "/user/123?to=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with invalid to as UnixNano", - token: validToken, - url: fmt.Sprintf("/user/123?to=%d", time.Now().UnixNano()), - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with attributes", - token: validToken, - url: fmt.Sprintf("/user/123?with_attributes=%s", strconv.FormatBool(true)), - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid attributes", - token: validToken, - url: "/user/123?with_attributes=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with metadata", - token: validToken, - url: fmt.Sprintf("/user/123?with_metadata=%s", strconv.FormatBool(true)), - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid metadata", - token: validToken, - url: "/user/123?with_metadata=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with asc direction", - token: validToken, - url: "/user/123?dir=asc", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with desc direction", - token: validToken, - url: "/user/123?dir=desc", - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with invalid direction", - token: validToken, - url: "/user/123?dir=ten", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with malformed direction", - token: validToken, - url: "/user/123?dir=invalid&dir=invalid2", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with empty url", - token: validToken, - url: "", - status: http.StatusNotFound, - svcErr: nil, - }, - { - desc: "with empty entity type", - token: validToken, - url: "//123", - status: http.StatusNotFound, - svcErr: nil, - }, - { - desc: "with empty entity ID", - token: validToken, - url: "/user/", - status: http.StatusNotFound, - svcErr: nil, - }, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - if c.token == validToken { - c.session = smqauthn.Session{ - UserID: testsutil.GenerateUUID(t), - } - } - authCall := authn.On("Authenticate", mock.Anything, c.token).Return(c.session, nil) - svcCall := svc.On("RetrieveAll", mock.Anything, c.session, mock.Anything).Return(journal.JournalsPage{}, c.svcErr) - req := testRequest{ - client: es.Client(), - method: http.MethodGet, - url: es.URL + "/journal" + c.url, - token: c.token, - } - - resp, err := req.make() - assert.Nil(t, err, c.desc) - defer resp.Body.Close() - assert.Equal(t, c.status, resp.StatusCode, c.desc) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestListEntityJournalsEndpoint(t *testing.T) { - es, svc, authn := newjournalServer() - - domainID := testsutil.GenerateUUID(t) - userID := testsutil.GenerateUUID(t) - - cases := []struct { - desc string - token string - session smqauthn.Session - domainID string - url string - contentType string - page journal.Page - status int - authnErr error - svcErr error - }{ - { - desc: "with group type successful", - token: validToken, - domainID: domainID, - url: "/group/" + validID, - page: journal.Page{ - EntityID: validID, - EntityType: journal.GroupEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with channel type successful", - token: validToken, - domainID: domainID, - url: "/channel/" + validID, - page: journal.Page{ - EntityID: validID, - EntityType: journal.ChannelEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with client type successful", - token: validToken, - domainID: domainID, - url: "/client/" + validID, - page: journal.Page{ - EntityID: validID, - EntityType: journal.ClientEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with service error", - token: validToken, - domainID: domainID, - url: "/client/" + validID, - page: journal.Page{ - EntityID: validID, - EntityType: journal.ClientEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - }, - status: http.StatusForbidden, - svcErr: svcerr.ErrAuthorization, - }, - { - desc: "with channel operation", - token: validToken, - domainID: domainID, - url: "/channel/" + validID + "?operation=channel.create", - page: journal.Page{ - EntityID: validID, - EntityType: journal.ChannelEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - Operation: "channel.create", - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with group operation", - token: validToken, - domainID: domainID, - url: "/group/" + validID + "?operation=group.create", - page: journal.Page{ - EntityID: validID, - EntityType: journal.GroupEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - Operation: "group.create", - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with client operation", - token: validToken, - domainID: domainID, - url: "/client/" + validID + "?operation=client.create", - page: journal.Page{ - EntityID: validID, - EntityType: journal.ClientEntity, - Offset: 0, - Limit: 10, - Direction: api.DescDir, - Operation: "client.create", - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with malformed operation", - token: validToken, - domainID: domainID, - url: "/user/123?operation=user.create&operation=user.update", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with invalid entity type", - token: validToken, - domainID: domainID, - url: "/invalid/123", - status: http.StatusBadRequest, - svcErr: nil, - }, - { - desc: "with all query params", - token: validToken, - domainID: domainID, - url: "/group/" + validID + "?offset=10&limit=10&operation=group.create&from=0&to=10&with_attributes=true&with_metadata=true&dir=asc", - page: journal.Page{ - EntityID: validID, - EntityType: journal.GroupEntity, - Offset: 10, - Limit: 10, - Operation: "group.create", - From: time.Time{}, - To: time.Unix(10, 0), - WithAttributes: true, - WithMetadata: true, - Direction: api.AscDir, - }, - status: http.StatusOK, - svcErr: nil, - }, - { - desc: " with empty token", - url: "/group/123", - domainID: domainID, - status: http.StatusUnauthorized, - svcErr: nil, - }, - { - desc: "with empty domain ID", - token: validToken, - url: "/group/", - status: http.StatusBadRequest, - svcErr: nil, - }, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - if c.token == validToken { - c.session = smqauthn.Session{ - UserID: userID, - DomainID: domainID, - DomainUserID: domainID + "_" + userID, - } - } - authCall := authn.On("Authenticate", mock.Anything, c.token).Return(c.session, c.authnErr) - svcCall := svc.On("RetrieveAll", mock.Anything, c.session, c.page).Return(journal.JournalsPage{}, c.svcErr) - req := testRequest{ - client: es.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/%s/journal%s", es.URL, c.domainID, c.url), - token: c.token, - } - resp, err := req.make() - assert.Nil(t, err, c.desc) - defer resp.Body.Close() - assert.Equal(t, c.status, resp.StatusCode, c.desc) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestRetrieveClientTelemetryEndpoint(t *testing.T) { - es, svc, authn := newjournalServer() - - clientID := testsutil.GenerateUUID(t) - userID := testsutil.GenerateUUID(t) - domanID := testsutil.GenerateUUID(t) - - cases := []struct { - desc string - token string - session smqauthn.Session - clientID string - domainID string - url string - contentType string - status int - authnErr error - svcErr error - }{ - { - desc: "successful", - token: validToken, - clientID: clientID, - domainID: domanID, - url: fmt.Sprintf("/client/%s/telemetry", clientID), - status: http.StatusOK, - svcErr: nil, - }, - { - desc: "with service error", - token: validToken, - clientID: clientID, - domainID: domanID, - url: fmt.Sprintf("/client/%s/telemetry", clientID), - status: http.StatusForbidden, - svcErr: svcerr.ErrAuthorization, - }, - { - desc: "with empty token", - clientID: clientID, - domainID: domanID, - url: fmt.Sprintf("/client/%s/telemetry", clientID), - status: http.StatusUnauthorized, - svcErr: nil, - }, - { - desc: "with invalid client ID", - token: validToken, - domainID: domanID, - clientID: "invalid", - url: "/client/invalid/telemetry", - status: http.StatusNotFound, - svcErr: svcerr.ErrNotFound, - }, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - if c.token == validToken { - c.session = smqauthn.Session{ - UserID: userID, - DomainID: c.domainID, - DomainUserID: c.domainID + "_" + userID, - } - } - authCall := authn.On("Authenticate", mock.Anything, c.token).Return(c.session, c.authnErr) - svcCall := svc.On("RetrieveClientTelemetry", mock.Anything, c.session, c.clientID).Return(journal.ClientTelemetry{}, c.svcErr) - req := testRequest{ - client: es.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/%s/journal%s", es.URL, c.domainID, c.url), - token: c.token, - } - resp, err := req.make() - assert.Nil(t, err, c.desc) - defer resp.Body.Close() - assert.Equal(t, c.status, resp.StatusCode, c.desc) - svcCall.Unset() - authCall.Unset() - }) - } -} diff --git a/journal/api/requests.go b/journal/api/requests.go deleted file mode 100644 index ec21af70b..000000000 --- a/journal/api/requests.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/journal" -) - -type retrieveJournalsReq struct { - token string - page journal.Page -} - -func (req retrieveJournalsReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.page.Limit > api.DefLimit { - return apiutil.ErrLimitSize - } - if req.page.Direction != "" && req.page.Direction != api.AscDir && req.page.Direction != api.DescDir { - return apiutil.ErrInvalidDirection - } - if req.page.EntityID == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type retrieveClientTelemetryReq struct { - clientID string -} - -func (req retrieveClientTelemetryReq) validate() error { - if req.clientID == "" { - return apiutil.ErrMissingID - } - - return nil -} diff --git a/journal/api/requests_test.go b/journal/api/requests_test.go deleted file mode 100644 index 636b1db70..000000000 --- a/journal/api/requests_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "testing" - - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/journal" - "github.com/stretchr/testify/assert" -) - -var ( - token = "token" - limit uint64 = 10 -) - -func TestRetrieveJournalsReqValidate(t *testing.T) { - cases := []struct { - desc string - req retrieveJournalsReq - err error - }{ - { - desc: "valid", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - EntityID: "id", - EntityType: journal.UserEntity, - }, - }, - err: nil, - }, - { - desc: "missing token", - req: retrieveJournalsReq{ - page: journal.Page{ - Limit: limit, - EntityID: "id", - EntityType: journal.UserEntity, - }, - }, - err: apiutil.ErrBearerToken, - }, - { - desc: "invalid limit size", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: api.DefLimit + 1, - EntityID: "id", - EntityType: journal.UserEntity, - }, - }, - err: apiutil.ErrLimitSize, - }, - { - desc: "invalid sorting direction", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - Direction: "invalid", - EntityID: "id", - EntityType: journal.UserEntity, - }, - }, - err: apiutil.ErrInvalidDirection, - }, - { - desc: "valid id and entity type", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - EntityID: "id", - EntityType: journal.UserEntity, - }, - }, - err: nil, - }, - { - desc: "valid id and empty entity type", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - EntityID: "id", - }, - }, - err: nil, - }, - { - desc: "empty id and empty entity type", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - }, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty id and valid entity type", - req: retrieveJournalsReq{ - token: token, - page: journal.Page{ - Limit: limit, - EntityType: journal.UserEntity, - }, - }, - err: apiutil.ErrMissingID, - }, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - err := c.req.validate() - assert.Equal(t, c.err, err) - }) - } -} - -func TestRetrieveClientTelemetryReqValidate(t *testing.T) { - cases := []struct { - desc string - req retrieveClientTelemetryReq - err error - }{ - { - desc: "valid", - req: retrieveClientTelemetryReq{ - clientID: "id", - }, - err: nil, - }, - { - desc: "missing client id", - req: retrieveClientTelemetryReq{ - clientID: "", - }, - err: apiutil.ErrMissingID, - }, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - err := c.req.validate() - assert.Equal(t, c.err, err) - }) - } -} diff --git a/journal/api/responses.go b/journal/api/responses.go deleted file mode 100644 index fa95da415..000000000 --- a/journal/api/responses.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "net/http" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/journal" -) - -var ( - _ magistrala.Response = (*pageRes)(nil) - _ magistrala.Response = (*clientTelemetryRes)(nil) -) - -type pageRes struct { - journal.JournalsPage `json:",inline"` -} - -func (res pageRes) Headers() map[string]string { - return map[string]string{} -} - -func (res pageRes) Code() int { - return http.StatusOK -} - -func (res pageRes) Empty() bool { - return false -} - -type clientTelemetryRes struct { - journal.ClientTelemetry `json:",inline"` -} - -func (res clientTelemetryRes) Headers() map[string]string { - return map[string]string{} -} - -func (res clientTelemetryRes) Code() int { - return http.StatusOK -} - -func (res clientTelemetryRes) Empty() bool { - return false -} diff --git a/journal/api/transport.go b/journal/api/transport.go deleted file mode 100644 index 3f38c3c3d..000000000 --- a/journal/api/transport.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "log/slog" - "math" - "net/http" - "time" - - "github.com/absmach/magistrala" - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/journal" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/go-chi/chi/v5" - kithttp "github.com/go-kit/kit/transport/http" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -const ( - operationKey = "operation" - fromKey = "from" - toKey = "to" - attributesKey = "with_attributes" - metadataKey = "with_metadata" -) - -// MakeHandler returns a HTTP API handler with health check and metrics. -func MakeHandler(svc journal.Service, authn smqauthn.AuthNMiddleware, logger *slog.Logger, svcName, instanceID string) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - - mux := chi.NewRouter() - idp := uuid.New() - mux.Use(api.RequestIDMiddleware(idp)) - - mux.With(authn.WithOptions(smqauthn.WithDomainCheck(false)).Middleware()).Get("/journal/user/{userID}", otelhttp.NewHandler(kithttp.NewServer( - retrieveJournalsEndpoint(svc), - decodeRetrieveUserJournalReq, - api.EncodeResponse, - opts..., - ), "list_user_journals").ServeHTTP) - - mux.Route("/{domainID}/journal", func(r chi.Router) { - r.Use(authn.Middleware()) - - r.Get("/{entityType}/{entityID}", otelhttp.NewHandler(kithttp.NewServer( - retrieveJournalsEndpoint(svc), - decodeRetrieveEntityJournalReq, - api.EncodeResponse, - opts..., - ), "list__entity_journals").ServeHTTP) - - r.Get("/client/{clientID}/telemetry", otelhttp.NewHandler(kithttp.NewServer( - retrieveClientTelemetryEndpoint(svc), - decodeRetrieveClientTelemetryReq, - api.EncodeResponse, - opts..., - ), "view_client_telemetry").ServeHTTP) - }) - - mux.Get("/health", magistrala.Health(svcName, instanceID)) - mux.Handle("/metrics", promhttp.Handler()) - - return mux -} - -func decodeRetrieveEntityJournalReq(_ context.Context, r *http.Request) (any, error) { - page, err := decodePageQuery(r) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - entityType, err := journal.ToEntityType(chi.URLParam(r, "entityType")) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - page.EntityID = chi.URLParam(r, "entityID") - page.EntityType = entityType - - req := retrieveJournalsReq{ - token: apiutil.ExtractBearerToken(r), - page: page, - } - - return req, nil -} - -func decodeRetrieveUserJournalReq(_ context.Context, r *http.Request) (any, error) { - page, err := decodePageQuery(r) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - page.EntityID = chi.URLParam(r, "userID") - page.EntityType = journal.UserEntity - - req := retrieveJournalsReq{ - token: apiutil.ExtractBearerToken(r), - page: page, - } - - return req, nil -} - -func decodePageQuery(r *http.Request) (journal.Page, error) { - offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - operation, err := apiutil.ReadStringQuery(r, operationKey, "") - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - from, err := apiutil.ReadNumQuery[int64](r, fromKey, 0) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - if from > math.MaxInt32 { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidTimeFormat) - } - var fromTime time.Time - if from != 0 { - fromTime = time.Unix(from, 0) - } - to, err := apiutil.ReadNumQuery[int64](r, toKey, 0) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - if to > math.MaxInt32 { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidTimeFormat) - } - var toTime time.Time - if to != 0 { - toTime = time.Unix(to, 0) - } - attributes, err := apiutil.ReadBoolQuery(r, attributesKey, false) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - metadata, err := apiutil.ReadBoolQuery(r, metadataKey, false) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - dir, err := apiutil.ReadStringQuery(r, api.DirKey, api.DescDir) - if err != nil { - return journal.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - - return journal.Page{ - Offset: offset, - Limit: limit, - Operation: operation, - From: fromTime, - To: toTime, - WithAttributes: attributes, - WithMetadata: metadata, - Direction: dir, - }, nil -} - -func decodeRetrieveClientTelemetryReq(_ context.Context, r *http.Request) (any, error) { - req := retrieveClientTelemetryReq{ - clientID: chi.URLParam(r, "clientID"), - } - - return req, nil -} diff --git a/journal/doc.go b/journal/doc.go deleted file mode 100644 index 3b6860678..000000000 --- a/journal/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package journal contains the journal service. -// This service is responsible for storing events from the event store to a -// journal log repository. It is also responsible for providing a REST API to query events. -package journal diff --git a/journal/events/consumer.go b/journal/events/consumer.go deleted file mode 100644 index 890ba43e6..000000000 --- a/journal/events/consumer.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "context" - "errors" - "log/slog" - "time" - - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/events/store" -) - -var ( - ErrMissingOccurredAt = errors.New("missing occurred_at") - errMissingOperation = errors.New("missing operation") - errMissingAttributes = errors.New("missing attributes") - errMsg = "failed to save journal" -) - -// Start method starts consuming messages received from Event store. -func Start(ctx context.Context, consumer string, sub events.Subscriber, service journal.Service) error { - subCfg := events.SubscriberConfig{ - Consumer: consumer, - Stream: store.StreamAllEvents, - Handler: Handle(service), - } - - return sub.Subscribe(ctx, subCfg) -} - -func Handle(service journal.Service) handleFunc { - return func(ctx context.Context, event events.Event) error { - data, err := event.Encode() - if err != nil { - return err - } - - operation, ok := data["operation"].(string) - if !ok { - // Error is logged instead of being returned to avoid redelivering of the event. - slog.Error(errMsg, "error", errMissingOperation) - return nil - } - delete(data, "operation") - - if operation == "" { - slog.Error(errMsg, "error", errMissingOperation) - return nil - } - - occurredAt, ok := data["occurred_at"].(float64) - if !ok { - slog.Error(errMsg, "error", ErrMissingOccurredAt) - return nil - } - delete(data, "occurred_at") - - if occurredAt == 0 { - slog.Error(errMsg, "error", ErrMissingOccurredAt) - return nil - } - - metadata, ok := data["metadata"].(map[string]any) - if !ok { - metadata = make(map[string]any) - } - delete(data, "metadata") - - if len(data) == 0 { - slog.Error(errMsg, "error", errMissingAttributes) - return nil - } - - j := journal.Journal{ - Operation: operation, - OccurredAt: time.Unix(0, int64(occurredAt)), - Attributes: data, - Metadata: metadata, - } - if err := service.Save(ctx, j); err != nil { - slog.Error(errMsg, "error", err) - } - - return nil - } -} - -type handleFunc func(ctx context.Context, event events.Event) error - -func (h handleFunc) Handle(ctx context.Context, event events.Event) error { - return h(ctx, event) -} - -func (h handleFunc) Cancel() error { - return nil -} diff --git a/journal/events/consumer_test.go b/journal/events/consumer_test.go deleted file mode 100644 index 0f0e28006..000000000 --- a/journal/events/consumer_test.go +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events_test - -import ( - "context" - "encoding/json" - "errors" - "math/rand" - "strings" - "testing" - "time" - - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/journal" - aevents "github.com/absmach/magistrala/journal/events" - "github.com/absmach/magistrala/journal/mocks" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - operation = "users.create" - payload = map[string]any{ - "temperature": rand.Float64(), - "humidity": float64(rand.Intn(1000)), - "locations": []any{ - strings.Repeat("a", 100), - strings.Repeat("a", 100), - }, - "status": "active", - } - idProvider = uuid.New() -) - -type testEvent struct { - data map[string]any - err error -} - -func (e testEvent) Encode() (map[string]any, error) { - return e.data, e.err -} - -func NewTestEvent(data map[string]any, err error) testEvent { - return testEvent{data: data, err: err} -} - -func TestHandle(t *testing.T) { - repo := new(mocks.Repository) - svc := journal.NewService(idProvider, repo) - - cases := []struct { - desc string - event map[string]any - encodeErr error - repoErr error - err error - }{ - { - desc: "success", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with encode error", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - encodeErr: errors.New("encode error"), - err: errors.New("encode error"), - }, - { - desc: "with missing operation", - event: map[string]any{ - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with empty operation", - event: map[string]any{ - "operation": "", - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with invalid operation", - event: map[string]any{ - "operation": 1, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with missing occurred_at", - event: map[string]any{ - "operation": operation, - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with empty occurred_at", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(0), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with invalid occurred_at", - event: map[string]any{ - "operation": operation, - "occurred_at": "invalid", - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with missing metadata", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - }, - err: nil, - }, - { - desc: "with empty metadata", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": map[string]any{}, - }, - err: nil, - }, - { - desc: "with invalid metadata", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": 1, - }, - err: nil, - }, - { - desc: "with missing attributes", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with empty attributes", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": "", - "tags": []any{}, - "number": float64(0), - "metadata": payload, - }, - err: nil, - }, - { - desc: "with invalid attributes", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - "nested": map[string]any{ - "key": float64(rand.Intn(1000)), - }, - }, - }, - }, - }, - }, - "metadata": payload, - }, - err: nil, - }, - { - desc: "success", - event: map[string]any{ - "operation": operation, - "occurred_at": float64(time.Now().UnixNano()), - "id": testsutil.GenerateUUID(t), - "tags": []any{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - "number": float64(rand.Intn(1000)), - "metadata": payload, - }, - repoErr: repoerr.ErrCreateEntity, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data, err := json.Marshal(tc.event) - assert.NoError(t, err) - - event := map[string]any{} - err = json.Unmarshal(data, &event) - assert.NoError(t, err) - - repoCall := repo.On("Save", context.Background(), mock.Anything).Return(tc.repoErr) - err = aevents.Handle(svc)(context.Background(), NewTestEvent(event, tc.encodeErr)) - switch { - case tc.err == nil: - assert.NoError(t, err) - default: - assert.ErrorContains(t, err, tc.err.Error()) - } - repoCall.Unset() - }) - } -} diff --git a/journal/events/doc.go b/journal/events/doc.go deleted file mode 100644 index 5023696f8..000000000 --- a/journal/events/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package events provides the event consumer for the journal service. -// This package is responsible for consuming events from the event store and -// processing them. -package events diff --git a/journal/journal.go b/journal/journal.go deleted file mode 100644 index bb211667e..000000000 --- a/journal/journal.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package journal - -import ( - "context" - "encoding/json" - "time" - - apiutil "github.com/absmach/magistrala/api/http/util" - smqauthn "github.com/absmach/magistrala/pkg/authn" -) - -type EntityType uint8 - -const ( - UserEntity EntityType = iota - GroupEntity - ClientEntity - ChannelEntity -) - -// String representation of the possible entity type values. -const ( - userEntityType = "user" - groupEntityType = "group" - clientEntityType = "client" - channelEntityType = "channel" -) - -// String converts entity type to string literal. -func (e EntityType) String() string { - switch e { - case UserEntity: - return userEntityType - case GroupEntity: - return groupEntityType - case ClientEntity: - return clientEntityType - case ChannelEntity: - return channelEntityType - default: - return "" - } -} - -// ToEntityType converts string value to a valid entity type. -func ToEntityType(entityType string) (EntityType, error) { - switch entityType { - case userEntityType: - return UserEntity, nil - case groupEntityType: - return GroupEntity, nil - case clientEntityType: - return ClientEntity, nil - case channelEntityType: - return ChannelEntity, nil - default: - return EntityType(0), apiutil.ErrInvalidEntityType - } -} - -// Query returns the SQL condition for the entity type. -func (e EntityType) Query() string { - switch e { - case UserEntity: - return "((operation LIKE 'user.%' AND attributes->>'id' = :entity_id) OR (attributes->>'user_id' = :entity_id))" - case GroupEntity: - return "((operation LIKE 'group.%' AND (attributes->>'id' = :entity_id OR attributes->>'entity_id' = :entity_id)))" - case ChannelEntity: - return "((operation LIKE 'channel.%' AND (attributes->>'id' = :entity_id OR attributes->>'entity_id' = :entity_id)) OR (jsonb_exists_any(attributes->'channel_ids', array[:entity_id])))" - case ClientEntity: - return "((operation LIKE 'client.%' AND (attributes->>'id' = :entity_id OR attributes->>'entity_id' = :entity_id)) OR (jsonb_exists_any(attributes->'client_ids', array[:entity_id])))" - default: - return "" - } -} - -// Journal represents an event journal that occurred in the system. -type Journal struct { - ID string `json:"id,omitempty" db:"id"` - Domain string `json:"domain,omitempty" db:"domain"` - Operation string `json:"operation,omitempty" db:"operation,omitempty"` - OccurredAt time.Time `json:"occurred_at,omitempty" db:"occurred_at,omitempty"` - Attributes map[string]any `json:"attributes,omitempty" db:"attributes,omitempty"` // This is extra information about the journal for example client_id, user_id, group_id etc. - Metadata map[string]any `json:"metadata,omitempty" db:"metadata,omitempty"` // This is decoded metadata from the journal. -} - -// JournalsPage represents a page of journals. -type JournalsPage struct { - Total uint64 `json:"total"` - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Journals []Journal `json:"journals"` -} - -// Page is used to filter journals. -type Page struct { - Offset uint64 `json:"offset" db:"offset"` - Limit uint64 `json:"limit" db:"limit"` - Operation string `json:"operation,omitempty" db:"operation,omitempty"` - From time.Time `json:"from,omitempty" db:"from,omitempty"` - To time.Time `json:"to,omitempty" db:"to,omitempty"` - WithAttributes bool `json:"with_attributes,omitempty"` - WithMetadata bool `json:"with_metadata,omitempty"` - EntityID string `json:"entity_id,omitempty" db:"entity_id,omitempty"` - EntityType EntityType `json:"entity_type,omitempty" db:"entity_type,omitempty"` - Direction string `json:"direction,omitempty"` -} - -func (page JournalsPage) MarshalJSON() ([]byte, error) { - type Alias JournalsPage - a := struct { - Alias - }{ - Alias: Alias(page), - } - - if a.Journals == nil { - a.Journals = make([]Journal, 0) - } - - return json.Marshal(a) -} - -type ClientTelemetry struct { - ClientID string `json:"client_id"` - DomainID string `json:"domain_id"` - Subscriptions uint64 `json:"subscriptions"` - InboundMessages uint64 `json:"inbound_messages"` - OutboundMessages uint64 `json:"outbound_messages"` - FirstSeen time.Time `json:"first_seen"` - LastSeen time.Time `json:"last_seen"` -} - -type ClientSubscription struct { - ID string `json:"id" db:"id"` - SubscriberID string `json:"subscriber_id" db:"subscriber_id"` - ChannelID string `json:"channel_id" db:"channel_id"` - Subtopic string `json:"subtopic" db:"subtopic"` - ClientID string `json:"client_id" db:"client_id"` -} - -// Service provides access to the journal log service. -type Service interface { - // Save saves the journal to the database. - Save(ctx context.Context, journal Journal) error - - // RetrieveAll retrieves all journals from the database with the given page. - RetrieveAll(ctx context.Context, session smqauthn.Session, page Page) (JournalsPage, error) - - // RetrieveClientTelemetry retrieves telemetry data for a client. - RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (ClientTelemetry, error) -} - -// Repository provides access to the journal log database. -type Repository interface { - // Save persists the journal to a database. - Save(ctx context.Context, journal Journal) error - - // RetrieveAll retrieves all journals from the database with the given page. - RetrieveAll(ctx context.Context, page Page) (JournalsPage, error) - - // SaveClientTelemetry persists telemetry data for a client to the database. - SaveClientTelemetry(ctx context.Context, ct ClientTelemetry) error - - // RetrieveClientTelemetry retrieves telemetry data for a client from the database. - RetrieveClientTelemetry(ctx context.Context, clientID, domainID string) (ClientTelemetry, error) - - // DeleteClientTelemetry removes telemetry data for a client from the database. - DeleteClientTelemetry(ctx context.Context, clientID, domainID string) error - - // AddSubscription adds a subscription to the client telemetry. - AddSubscription(ctx context.Context, sub ClientSubscription) error - - // CountSubscriptions returns the number of subscriptions for a client. - CountSubscriptions(ctx context.Context, clientID string) (uint64, error) - - // RemoveSubscription removes a subscription from the client telemetry. - RemoveSubscription(ctx context.Context, subscriberID string) error - - // IncrementInboundMessages increments the inbound messages count for a client. - IncrementInboundMessages(ctx context.Context, ct ClientTelemetry) error - - // IncrementOutboundMessages increments the outbound messages count for a client. - IncrementOutboundMessages(ctx context.Context, channelID, subtopic string) error -} diff --git a/journal/journal_test.go b/journal/journal_test.go deleted file mode 100644 index 3d855f853..000000000 --- a/journal/journal_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package journal_test - -import ( - "fmt" - "testing" - "time" - - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/journal" - "github.com/stretchr/testify/assert" -) - -func TestJournalsPage_MarshalJSON(t *testing.T) { - occurredAt := time.Now() - - cases := []struct { - desc string - page journal.JournalsPage - res string - }{ - { - desc: "empty page", - page: journal.JournalsPage{ - Journals: []journal.Journal(nil), - }, - res: `{"total":0,"offset":0,"limit":0,"journals":[]}`, - }, - { - desc: "page with journals", - page: journal.JournalsPage{ - Total: 1, - Offset: 0, - Limit: 0, - Journals: []journal.Journal{ - { - Operation: "123", - OccurredAt: occurredAt, - Attributes: map[string]any{"123": "123"}, - Metadata: map[string]any{"123": "123"}, - }, - }, - }, - res: fmt.Sprintf(`{"total":1,"offset":0,"limit":0,"journals":[{"operation":"123","occurred_at":"%s","attributes":{"123":"123"},"metadata":{"123":"123"}}]}`, occurredAt.Format(time.RFC3339Nano)), - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data, err := tc.page.MarshalJSON() - assert.NoError(t, err, "Unexpected error: %v", err) - assert.Equal(t, tc.res, string(data)) - }) - } -} - -func TestEntityType(t *testing.T) { - cases := []struct { - desc string - e journal.EntityType - str string - queryString string - }{ - { - desc: "UserEntity", - e: journal.UserEntity, - str: "user", - }, - { - desc: "ClientEntity", - e: journal.ClientEntity, - str: "client", - }, - { - desc: "GroupEntity", - e: journal.GroupEntity, - str: "group", - }, - { - desc: "ChannelEntity", - e: journal.ChannelEntity, - str: "channel", - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - assert.Equal(t, tc.str, tc.e.String()) - assert.NotEmpty(t, tc.e.Query()) - }) - } -} - -func TestToEntityType(t *testing.T) { - cases := []struct { - desc string - entityType string - expected journal.EntityType - expectedErr error - }{ - { - desc: "UserEntity", - entityType: "user", - expected: journal.UserEntity, - }, - { - desc: "ClientEntity", - entityType: "client", - expected: journal.ClientEntity, - }, - { - desc: "GroupEntity", - entityType: "group", - expected: journal.GroupEntity, - }, - { - desc: "ChannelEntity", - entityType: "channel", - expected: journal.ChannelEntity, - }, - { - desc: "Invalid entity type", - entityType: "invalid", - expectedErr: apiutil.ErrInvalidEntityType, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - entityType, err := journal.ToEntityType(tc.entityType) - assert.Equal(t, tc.expected, entityType) - assert.Equal(t, tc.expectedErr, err) - }) - } -} diff --git a/journal/middleware/authorization.go b/journal/middleware/authorization.go deleted file mode 100644 index 065b01fb2..000000000 --- a/journal/middleware/authorization.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/journal" - smqauthn "github.com/absmach/magistrala/pkg/authn" - smqauthz "github.com/absmach/magistrala/pkg/authz" - "github.com/absmach/magistrala/pkg/policies" -) - -var ( - _ journal.Service = (*authorizationMiddleware)(nil) - - readPermission = "read_permission" -) - -type authorizationMiddleware struct { - svc journal.Service - authz smqauthz.Authorization -} - -// NewAuthorization adds authorization to the journal service. -func NewAuthorization(svc journal.Service, authz smqauthz.Authorization) journal.Service { - return &authorizationMiddleware{ - svc: svc, - authz: authz, - } -} - -func (am *authorizationMiddleware) Save(ctx context.Context, journal journal.Journal) error { - return am.svc.Save(ctx, journal) -} - -func (am *authorizationMiddleware) RetrieveAll(ctx context.Context, session smqauthn.Session, page journal.Page) (journal.JournalsPage, error) { - permission := readPermission - objectType := page.EntityType.String() - object := page.EntityID - subject := subjectID(session) - - // If the entity is a user, we need to check if the user is an admin - if page.EntityType.String() == policies.UserType { - permission = policies.AdminPermission - objectType = policies.PlatformType - object = policies.MagistralaObject - subject = session.UserID - } - - req := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: subject, - Permission: permission, - ObjectType: objectType, - Object: object, - } - if err := am.authz.Authorize(ctx, req, nil); err != nil { - return journal.JournalsPage{}, err - } - - return am.svc.RetrieveAll(ctx, session, page) -} - -func (am *authorizationMiddleware) RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (journal.ClientTelemetry, error) { - req := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: subjectID(session), - Permission: readPermission, - ObjectType: policies.ClientType, - Object: clientID, - } - - if err := am.authz.Authorize(ctx, req, nil); err != nil { - return journal.ClientTelemetry{}, err - } - - return am.svc.RetrieveClientTelemetry(ctx, session, clientID) -} - -func subjectID(session smqauthn.Session) string { - if session.UserID != "" { - return session.UserID - } - return session.DomainUserID -} diff --git a/journal/middleware/doc.go b/journal/middleware/doc.go deleted file mode 100644 index 9740b7331..000000000 --- a/journal/middleware/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package middleware provides tracing, logging and metrics middleware -// for Magistrala Journal service. -// -// For more details about tracing instrumentation for Magistrala refer to the -// documentation at https://magistrala.absmach.eu/docs/. -package middleware diff --git a/journal/middleware/logging.go b/journal/middleware/logging.go deleted file mode 100644 index 1287b8777..000000000 --- a/journal/middleware/logging.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/journal" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/go-chi/chi/v5/middleware" -) - -var _ journal.Service = (*loggingMiddleware)(nil) - -type loggingMiddleware struct { - logger *slog.Logger - service journal.Service -} - -// NewLogging adds logging facilities to the adapter. -func NewLogging(service journal.Service, logger *slog.Logger) journal.Service { - return &loggingMiddleware{ - logger: logger, - service: service, - } -} - -func (lm *loggingMiddleware) Save(ctx context.Context, j journal.Journal) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.Group("journal", - slog.String("occurred_at", j.OccurredAt.Format(time.RFC3339Nano)), - slog.String("operation", j.Operation), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Save journal failed", args...) - return - } - lm.logger.Info("Save journal completed successfully", args...) - }(time.Now()) - - return lm.service.Save(ctx, j) -} - -func (lm *loggingMiddleware) RetrieveAll(ctx context.Context, session smqauthn.Session, page journal.Page) (journalsPage journal.JournalsPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.Group("page", - slog.String("operation", page.Operation), - slog.String("entity_type", page.EntityType.String()), - slog.Uint64("offset", page.Offset), - slog.Uint64("limit", page.Limit), - slog.Uint64("total", journalsPage.Total), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Retrieve all journals failed", args...) - return - } - lm.logger.Info("Retrieve all journals completed successfully", args...) - }(time.Now()) - - return lm.service.RetrieveAll(ctx, session, page) -} - -func (lm *loggingMiddleware) RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (ct journal.ClientTelemetry, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("request_id", middleware.GetReqID(ctx)), - slog.String("client_id", clientID), - slog.String("domain_id", session.DomainID), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Retrieve client telemetry failed", args...) - return - } - lm.logger.Info("Retrieve client telemetry completed successfully", args...) - }(time.Now()) - - return lm.service.RetrieveClientTelemetry(ctx, session, clientID) -} diff --git a/journal/middleware/metrics.go b/journal/middleware/metrics.go deleted file mode 100644 index 7ce0be97f..000000000 --- a/journal/middleware/metrics.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/journal" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/go-kit/kit/metrics" -) - -var _ journal.Service = (*metricsMiddleware)(nil) - -type metricsMiddleware struct { - counter metrics.Counter - latency metrics.Histogram - service journal.Service -} - -// NewMetrics returns new message repository with Save method wrapped to expose metrics. -func NewMetrics(service journal.Service, counter metrics.Counter, latency metrics.Histogram) journal.Service { - return &metricsMiddleware{ - counter: counter, - latency: latency, - service: service, - } -} - -func (mm *metricsMiddleware) Save(ctx context.Context, j journal.Journal) error { - defer func(begin time.Time) { - mm.counter.With("method", "save").Add(1) - mm.latency.With("method", "save").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.Save(ctx, j) -} - -func (mm *metricsMiddleware) RetrieveAll(ctx context.Context, session smqauthn.Session, page journal.Page) (journal.JournalsPage, error) { - defer func(begin time.Time) { - mm.counter.With("method", "retrieve_all").Add(1) - mm.latency.With("method", "retrieve_all").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.RetrieveAll(ctx, session, page) -} - -func (mm *metricsMiddleware) RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (journal.ClientTelemetry, error) { - defer func(begin time.Time) { - mm.counter.With("method", "retrieve_client_telemetry").Add(1) - mm.latency.With("method", "retrieve_client_telemetry").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.RetrieveClientTelemetry(ctx, session, clientID) -} diff --git a/journal/middleware/tracing.go b/journal/middleware/tracing.go deleted file mode 100644 index 431789cdc..000000000 --- a/journal/middleware/tracing.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/journal" - smqauthn "github.com/absmach/magistrala/pkg/authn" - smqTracing "github.com/absmach/magistrala/pkg/tracing" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -var _ journal.Service = (*tracing)(nil) - -type tracing struct { - tracer trace.Tracer - svc journal.Service -} - -// NewTracing returns a new journal service with tracing capabilities. -func NewTracing(svc journal.Service, tracer trace.Tracer) journal.Service { - return &tracing{tracer, svc} -} - -func (tm *tracing) Save(ctx context.Context, j journal.Journal) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "save", trace.WithAttributes( - attribute.String("occurred_at", j.OccurredAt.String()), - attribute.String("operation", j.Operation), - )) - defer span.End() - - return tm.svc.Save(ctx, j) -} - -func (tm *tracing) RetrieveAll(ctx context.Context, session smqauthn.Session, page journal.Page) (resp journal.JournalsPage, err error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "retrieve_all", trace.WithAttributes( - attribute.Int64("offset", int64(page.Offset)), - attribute.Int64("limit", int64(page.Limit)), - attribute.Int64("total", int64(resp.Total)), - attribute.String("entity_type", page.EntityType.String()), - attribute.String("operation", page.Operation), - )) - defer span.End() - - return tm.svc.RetrieveAll(ctx, session, page) -} - -func (tm *tracing) RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (j journal.ClientTelemetry, err error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "retrieve", trace.WithAttributes( - attribute.String("client_id", clientID), - attribute.String("domain_id", session.DomainID), - )) - defer span.End() - - return tm.svc.RetrieveClientTelemetry(ctx, session, clientID) -} diff --git a/journal/mocks/doc.go b/journal/mocks/doc.go deleted file mode 100644 index 16ed198af..000000000 --- a/journal/mocks/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package mocks contains mocks for testing purposes. -package mocks diff --git a/journal/mocks/repository.go b/journal/mocks/repository.go deleted file mode 100644 index c9e14c877..000000000 --- a/journal/mocks/repository.go +++ /dev/null @@ -1,658 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/journal" - mock "github.com/stretchr/testify/mock" -) - -// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -type Repository_Expecter struct { - mock *mock.Mock -} - -func (_m *Repository) EXPECT() *Repository_Expecter { - return &Repository_Expecter{mock: &_m.Mock} -} - -// AddSubscription provides a mock function for the type Repository -func (_mock *Repository) AddSubscription(ctx context.Context, sub journal.ClientSubscription) error { - ret := _mock.Called(ctx, sub) - - if len(ret) == 0 { - panic("no return value specified for AddSubscription") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.ClientSubscription) error); ok { - r0 = returnFunc(ctx, sub) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_AddSubscription_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddSubscription' -type Repository_AddSubscription_Call struct { - *mock.Call -} - -// AddSubscription is a helper method to define mock.On call -// - ctx context.Context -// - sub journal.ClientSubscription -func (_e *Repository_Expecter) AddSubscription(ctx interface{}, sub interface{}) *Repository_AddSubscription_Call { - return &Repository_AddSubscription_Call{Call: _e.mock.On("AddSubscription", ctx, sub)} -} - -func (_c *Repository_AddSubscription_Call) Run(run func(ctx context.Context, sub journal.ClientSubscription)) *Repository_AddSubscription_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.ClientSubscription - if args[1] != nil { - arg1 = args[1].(journal.ClientSubscription) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_AddSubscription_Call) Return(err error) *Repository_AddSubscription_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_AddSubscription_Call) RunAndReturn(run func(ctx context.Context, sub journal.ClientSubscription) error) *Repository_AddSubscription_Call { - _c.Call.Return(run) - return _c -} - -// CountSubscriptions provides a mock function for the type Repository -func (_mock *Repository) CountSubscriptions(ctx context.Context, clientID string) (uint64, error) { - ret := _mock.Called(ctx, clientID) - - if len(ret) == 0 { - panic("no return value specified for CountSubscriptions") - } - - var r0 uint64 - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (uint64, error)); ok { - return returnFunc(ctx, clientID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) uint64); ok { - r0 = returnFunc(ctx, clientID) - } else { - r0 = ret.Get(0).(uint64) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = returnFunc(ctx, clientID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_CountSubscriptions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountSubscriptions' -type Repository_CountSubscriptions_Call struct { - *mock.Call -} - -// CountSubscriptions is a helper method to define mock.On call -// - ctx context.Context -// - clientID string -func (_e *Repository_Expecter) CountSubscriptions(ctx interface{}, clientID interface{}) *Repository_CountSubscriptions_Call { - return &Repository_CountSubscriptions_Call{Call: _e.mock.On("CountSubscriptions", ctx, clientID)} -} - -func (_c *Repository_CountSubscriptions_Call) Run(run func(ctx context.Context, clientID string)) *Repository_CountSubscriptions_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_CountSubscriptions_Call) Return(v uint64, err error) *Repository_CountSubscriptions_Call { - _c.Call.Return(v, err) - return _c -} - -func (_c *Repository_CountSubscriptions_Call) RunAndReturn(run func(ctx context.Context, clientID string) (uint64, error)) *Repository_CountSubscriptions_Call { - _c.Call.Return(run) - return _c -} - -// DeleteClientTelemetry provides a mock function for the type Repository -func (_mock *Repository) DeleteClientTelemetry(ctx context.Context, clientID string, domainID string) error { - ret := _mock.Called(ctx, clientID, domainID) - - if len(ret) == 0 { - panic("no return value specified for DeleteClientTelemetry") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = returnFunc(ctx, clientID, domainID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_DeleteClientTelemetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClientTelemetry' -type Repository_DeleteClientTelemetry_Call struct { - *mock.Call -} - -// DeleteClientTelemetry is a helper method to define mock.On call -// - ctx context.Context -// - clientID string -// - domainID string -func (_e *Repository_Expecter) DeleteClientTelemetry(ctx interface{}, clientID interface{}, domainID interface{}) *Repository_DeleteClientTelemetry_Call { - return &Repository_DeleteClientTelemetry_Call{Call: _e.mock.On("DeleteClientTelemetry", ctx, clientID, domainID)} -} - -func (_c *Repository_DeleteClientTelemetry_Call) Run(run func(ctx context.Context, clientID string, domainID string)) *Repository_DeleteClientTelemetry_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_DeleteClientTelemetry_Call) Return(err error) *Repository_DeleteClientTelemetry_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_DeleteClientTelemetry_Call) RunAndReturn(run func(ctx context.Context, clientID string, domainID string) error) *Repository_DeleteClientTelemetry_Call { - _c.Call.Return(run) - return _c -} - -// IncrementInboundMessages provides a mock function for the type Repository -func (_mock *Repository) IncrementInboundMessages(ctx context.Context, ct journal.ClientTelemetry) error { - ret := _mock.Called(ctx, ct) - - if len(ret) == 0 { - panic("no return value specified for IncrementInboundMessages") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.ClientTelemetry) error); ok { - r0 = returnFunc(ctx, ct) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_IncrementInboundMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncrementInboundMessages' -type Repository_IncrementInboundMessages_Call struct { - *mock.Call -} - -// IncrementInboundMessages is a helper method to define mock.On call -// - ctx context.Context -// - ct journal.ClientTelemetry -func (_e *Repository_Expecter) IncrementInboundMessages(ctx interface{}, ct interface{}) *Repository_IncrementInboundMessages_Call { - return &Repository_IncrementInboundMessages_Call{Call: _e.mock.On("IncrementInboundMessages", ctx, ct)} -} - -func (_c *Repository_IncrementInboundMessages_Call) Run(run func(ctx context.Context, ct journal.ClientTelemetry)) *Repository_IncrementInboundMessages_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.ClientTelemetry - if args[1] != nil { - arg1 = args[1].(journal.ClientTelemetry) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_IncrementInboundMessages_Call) Return(err error) *Repository_IncrementInboundMessages_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_IncrementInboundMessages_Call) RunAndReturn(run func(ctx context.Context, ct journal.ClientTelemetry) error) *Repository_IncrementInboundMessages_Call { - _c.Call.Return(run) - return _c -} - -// IncrementOutboundMessages provides a mock function for the type Repository -func (_mock *Repository) IncrementOutboundMessages(ctx context.Context, channelID string, subtopic string) error { - ret := _mock.Called(ctx, channelID, subtopic) - - if len(ret) == 0 { - panic("no return value specified for IncrementOutboundMessages") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = returnFunc(ctx, channelID, subtopic) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_IncrementOutboundMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncrementOutboundMessages' -type Repository_IncrementOutboundMessages_Call struct { - *mock.Call -} - -// IncrementOutboundMessages is a helper method to define mock.On call -// - ctx context.Context -// - channelID string -// - subtopic string -func (_e *Repository_Expecter) IncrementOutboundMessages(ctx interface{}, channelID interface{}, subtopic interface{}) *Repository_IncrementOutboundMessages_Call { - return &Repository_IncrementOutboundMessages_Call{Call: _e.mock.On("IncrementOutboundMessages", ctx, channelID, subtopic)} -} - -func (_c *Repository_IncrementOutboundMessages_Call) Run(run func(ctx context.Context, channelID string, subtopic string)) *Repository_IncrementOutboundMessages_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_IncrementOutboundMessages_Call) Return(err error) *Repository_IncrementOutboundMessages_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_IncrementOutboundMessages_Call) RunAndReturn(run func(ctx context.Context, channelID string, subtopic string) error) *Repository_IncrementOutboundMessages_Call { - _c.Call.Return(run) - return _c -} - -// RemoveSubscription provides a mock function for the type Repository -func (_mock *Repository) RemoveSubscription(ctx context.Context, subscriberID string) error { - ret := _mock.Called(ctx, subscriberID) - - if len(ret) == 0 { - panic("no return value specified for RemoveSubscription") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, subscriberID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_RemoveSubscription_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveSubscription' -type Repository_RemoveSubscription_Call struct { - *mock.Call -} - -// RemoveSubscription is a helper method to define mock.On call -// - ctx context.Context -// - subscriberID string -func (_e *Repository_Expecter) RemoveSubscription(ctx interface{}, subscriberID interface{}) *Repository_RemoveSubscription_Call { - return &Repository_RemoveSubscription_Call{Call: _e.mock.On("RemoveSubscription", ctx, subscriberID)} -} - -func (_c *Repository_RemoveSubscription_Call) Run(run func(ctx context.Context, subscriberID string)) *Repository_RemoveSubscription_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_RemoveSubscription_Call) Return(err error) *Repository_RemoveSubscription_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_RemoveSubscription_Call) RunAndReturn(run func(ctx context.Context, subscriberID string) error) *Repository_RemoveSubscription_Call { - _c.Call.Return(run) - return _c -} - -// RetrieveAll provides a mock function for the type Repository -func (_mock *Repository) RetrieveAll(ctx context.Context, page journal.Page) (journal.JournalsPage, error) { - ret := _mock.Called(ctx, page) - - if len(ret) == 0 { - panic("no return value specified for RetrieveAll") - } - - var r0 journal.JournalsPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.Page) (journal.JournalsPage, error)); ok { - return returnFunc(ctx, page) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.Page) journal.JournalsPage); ok { - r0 = returnFunc(ctx, page) - } else { - r0 = ret.Get(0).(journal.JournalsPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, journal.Page) error); ok { - r1 = returnFunc(ctx, page) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll' -type Repository_RetrieveAll_Call struct { - *mock.Call -} - -// RetrieveAll is a helper method to define mock.On call -// - ctx context.Context -// - page journal.Page -func (_e *Repository_Expecter) RetrieveAll(ctx interface{}, page interface{}) *Repository_RetrieveAll_Call { - return &Repository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, page)} -} - -func (_c *Repository_RetrieveAll_Call) Run(run func(ctx context.Context, page journal.Page)) *Repository_RetrieveAll_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.Page - if args[1] != nil { - arg1 = args[1].(journal.Page) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_RetrieveAll_Call) Return(journalsPage journal.JournalsPage, err error) *Repository_RetrieveAll_Call { - _c.Call.Return(journalsPage, err) - return _c -} - -func (_c *Repository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, page journal.Page) (journal.JournalsPage, error)) *Repository_RetrieveAll_Call { - _c.Call.Return(run) - return _c -} - -// RetrieveClientTelemetry provides a mock function for the type Repository -func (_mock *Repository) RetrieveClientTelemetry(ctx context.Context, clientID string, domainID string) (journal.ClientTelemetry, error) { - ret := _mock.Called(ctx, clientID, domainID) - - if len(ret) == 0 { - panic("no return value specified for RetrieveClientTelemetry") - } - - var r0 journal.ClientTelemetry - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (journal.ClientTelemetry, error)); ok { - return returnFunc(ctx, clientID, domainID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) journal.ClientTelemetry); ok { - r0 = returnFunc(ctx, clientID, domainID) - } else { - r0 = ret.Get(0).(journal.ClientTelemetry) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = returnFunc(ctx, clientID, domainID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_RetrieveClientTelemetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveClientTelemetry' -type Repository_RetrieveClientTelemetry_Call struct { - *mock.Call -} - -// RetrieveClientTelemetry is a helper method to define mock.On call -// - ctx context.Context -// - clientID string -// - domainID string -func (_e *Repository_Expecter) RetrieveClientTelemetry(ctx interface{}, clientID interface{}, domainID interface{}) *Repository_RetrieveClientTelemetry_Call { - return &Repository_RetrieveClientTelemetry_Call{Call: _e.mock.On("RetrieveClientTelemetry", ctx, clientID, domainID)} -} - -func (_c *Repository_RetrieveClientTelemetry_Call) Run(run func(ctx context.Context, clientID string, domainID string)) *Repository_RetrieveClientTelemetry_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_RetrieveClientTelemetry_Call) Return(clientTelemetry journal.ClientTelemetry, err error) *Repository_RetrieveClientTelemetry_Call { - _c.Call.Return(clientTelemetry, err) - return _c -} - -func (_c *Repository_RetrieveClientTelemetry_Call) RunAndReturn(run func(ctx context.Context, clientID string, domainID string) (journal.ClientTelemetry, error)) *Repository_RetrieveClientTelemetry_Call { - _c.Call.Return(run) - return _c -} - -// Save provides a mock function for the type Repository -func (_mock *Repository) Save(ctx context.Context, journal1 journal.Journal) error { - ret := _mock.Called(ctx, journal1) - - if len(ret) == 0 { - panic("no return value specified for Save") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.Journal) error); ok { - r0 = returnFunc(ctx, journal1) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' -type Repository_Save_Call struct { - *mock.Call -} - -// Save is a helper method to define mock.On call -// - ctx context.Context -// - journal1 journal.Journal -func (_e *Repository_Expecter) Save(ctx interface{}, journal1 interface{}) *Repository_Save_Call { - return &Repository_Save_Call{Call: _e.mock.On("Save", ctx, journal1)} -} - -func (_c *Repository_Save_Call) Run(run func(ctx context.Context, journal1 journal.Journal)) *Repository_Save_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.Journal - if args[1] != nil { - arg1 = args[1].(journal.Journal) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_Save_Call) Return(err error) *Repository_Save_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_Save_Call) RunAndReturn(run func(ctx context.Context, journal1 journal.Journal) error) *Repository_Save_Call { - _c.Call.Return(run) - return _c -} - -// SaveClientTelemetry provides a mock function for the type Repository -func (_mock *Repository) SaveClientTelemetry(ctx context.Context, ct journal.ClientTelemetry) error { - ret := _mock.Called(ctx, ct) - - if len(ret) == 0 { - panic("no return value specified for SaveClientTelemetry") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.ClientTelemetry) error); ok { - r0 = returnFunc(ctx, ct) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_SaveClientTelemetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveClientTelemetry' -type Repository_SaveClientTelemetry_Call struct { - *mock.Call -} - -// SaveClientTelemetry is a helper method to define mock.On call -// - ctx context.Context -// - ct journal.ClientTelemetry -func (_e *Repository_Expecter) SaveClientTelemetry(ctx interface{}, ct interface{}) *Repository_SaveClientTelemetry_Call { - return &Repository_SaveClientTelemetry_Call{Call: _e.mock.On("SaveClientTelemetry", ctx, ct)} -} - -func (_c *Repository_SaveClientTelemetry_Call) Run(run func(ctx context.Context, ct journal.ClientTelemetry)) *Repository_SaveClientTelemetry_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.ClientTelemetry - if args[1] != nil { - arg1 = args[1].(journal.ClientTelemetry) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_SaveClientTelemetry_Call) Return(err error) *Repository_SaveClientTelemetry_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_SaveClientTelemetry_Call) RunAndReturn(run func(ctx context.Context, ct journal.ClientTelemetry) error) *Repository_SaveClientTelemetry_Call { - _c.Call.Return(run) - return _c -} diff --git a/journal/mocks/service.go b/journal/mocks/service.go deleted file mode 100644 index b3d7f8235..000000000 --- a/journal/mocks/service.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/pkg/authn" - mock "github.com/stretchr/testify/mock" -) - -// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewService(t interface { - mock.TestingT - Cleanup(func()) -}) *Service { - mock := &Service{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Service is an autogenerated mock type for the Service type -type Service struct { - mock.Mock -} - -type Service_Expecter struct { - mock *mock.Mock -} - -func (_m *Service) EXPECT() *Service_Expecter { - return &Service_Expecter{mock: &_m.Mock} -} - -// RetrieveAll provides a mock function for the type Service -func (_mock *Service) RetrieveAll(ctx context.Context, session authn.Session, page journal.Page) (journal.JournalsPage, error) { - ret := _mock.Called(ctx, session, page) - - if len(ret) == 0 { - panic("no return value specified for RetrieveAll") - } - - var r0 journal.JournalsPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, journal.Page) (journal.JournalsPage, error)); ok { - return returnFunc(ctx, session, page) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, journal.Page) journal.JournalsPage); ok { - r0 = returnFunc(ctx, session, page) - } else { - r0 = ret.Get(0).(journal.JournalsPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, journal.Page) error); ok { - r1 = returnFunc(ctx, session, page) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll' -type Service_RetrieveAll_Call struct { - *mock.Call -} - -// RetrieveAll is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - page journal.Page -func (_e *Service_Expecter) RetrieveAll(ctx interface{}, session interface{}, page interface{}) *Service_RetrieveAll_Call { - return &Service_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, session, page)} -} - -func (_c *Service_RetrieveAll_Call) Run(run func(ctx context.Context, session authn.Session, page journal.Page)) *Service_RetrieveAll_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 journal.Page - if args[2] != nil { - arg2 = args[2].(journal.Page) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_RetrieveAll_Call) Return(journalsPage journal.JournalsPage, err error) *Service_RetrieveAll_Call { - _c.Call.Return(journalsPage, err) - return _c -} - -func (_c *Service_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, page journal.Page) (journal.JournalsPage, error)) *Service_RetrieveAll_Call { - _c.Call.Return(run) - return _c -} - -// RetrieveClientTelemetry provides a mock function for the type Service -func (_mock *Service) RetrieveClientTelemetry(ctx context.Context, session authn.Session, clientID string) (journal.ClientTelemetry, error) { - ret := _mock.Called(ctx, session, clientID) - - if len(ret) == 0 { - panic("no return value specified for RetrieveClientTelemetry") - } - - var r0 journal.ClientTelemetry - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (journal.ClientTelemetry, error)); ok { - return returnFunc(ctx, session, clientID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) journal.ClientTelemetry); ok { - r0 = returnFunc(ctx, session, clientID) - } else { - r0 = ret.Get(0).(journal.ClientTelemetry) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, clientID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_RetrieveClientTelemetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveClientTelemetry' -type Service_RetrieveClientTelemetry_Call struct { - *mock.Call -} - -// RetrieveClientTelemetry is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - clientID string -func (_e *Service_Expecter) RetrieveClientTelemetry(ctx interface{}, session interface{}, clientID interface{}) *Service_RetrieveClientTelemetry_Call { - return &Service_RetrieveClientTelemetry_Call{Call: _e.mock.On("RetrieveClientTelemetry", ctx, session, clientID)} -} - -func (_c *Service_RetrieveClientTelemetry_Call) Run(run func(ctx context.Context, session authn.Session, clientID string)) *Service_RetrieveClientTelemetry_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_RetrieveClientTelemetry_Call) Return(clientTelemetry journal.ClientTelemetry, err error) *Service_RetrieveClientTelemetry_Call { - _c.Call.Return(clientTelemetry, err) - return _c -} - -func (_c *Service_RetrieveClientTelemetry_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, clientID string) (journal.ClientTelemetry, error)) *Service_RetrieveClientTelemetry_Call { - _c.Call.Return(run) - return _c -} - -// Save provides a mock function for the type Service -func (_mock *Service) Save(ctx context.Context, journal1 journal.Journal) error { - ret := _mock.Called(ctx, journal1) - - if len(ret) == 0 { - panic("no return value specified for Save") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, journal.Journal) error); ok { - r0 = returnFunc(ctx, journal1) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' -type Service_Save_Call struct { - *mock.Call -} - -// Save is a helper method to define mock.On call -// - ctx context.Context -// - journal1 journal.Journal -func (_e *Service_Expecter) Save(ctx interface{}, journal1 interface{}) *Service_Save_Call { - return &Service_Save_Call{Call: _e.mock.On("Save", ctx, journal1)} -} - -func (_c *Service_Save_Call) Run(run func(ctx context.Context, journal1 journal.Journal)) *Service_Save_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 journal.Journal - if args[1] != nil { - arg1 = args[1].(journal.Journal) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Service_Save_Call) Return(err error) *Service_Save_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_Save_Call) RunAndReturn(run func(ctx context.Context, journal1 journal.Journal) error) *Service_Save_Call { - _c.Call.Return(run) - return _c -} diff --git a/journal/postgres/doc.go b/journal/postgres/doc.go deleted file mode 100644 index 1007b3120..000000000 --- a/journal/postgres/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package postgres provides a postgres implementation of the journal log repository. -package postgres diff --git a/journal/postgres/errors.go b/journal/postgres/errors.go deleted file mode 100644 index 0d46fa3f4..000000000 --- a/journal/postgres/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import "github.com/absmach/magistrala/pkg/errors" - -var _ errors.Mapper = (*duplicateErrors)(nil) - -type duplicateErrors struct{} - -// GetError maps constraint names to known errors. -func (d duplicateErrors) GetError(constraint string) (error, bool) { - switch constraint { - case "journal_pkey": - return errors.NewRequestError("journal entry already exists"), true - default: - return nil, false - } -} - -func NewDuplicateErrors() errors.Mapper { - return duplicateErrors{} -} diff --git a/journal/postgres/init.go b/journal/postgres/init.go deleted file mode 100644 index ce9819cf0..000000000 --- a/journal/postgres/init.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - migrate "github.com/rubenv/sql-migrate" -) - -func Migration() *migrate.MemoryMigrationSource { - return &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "journal_01", - Up: []string{ - `CREATE TABLE IF NOT EXISTS journal ( - id VARCHAR(36) PRIMARY KEY, - operation VARCHAR NOT NULL, - domain VARCHAR, - occurred_at TIMESTAMP NOT NULL, - attributes JSONB NOT NULL, - metadata JSONB, - UNIQUE(operation, occurred_at, attributes) - )`, - `CREATE INDEX idx_journal_default_user_filter ON journal(operation, (attributes->>'id'), (attributes->>'user_id'), occurred_at DESC);`, - `CREATE INDEX idx_journal_default_group_filter ON journal(operation, (attributes->>'id'), (attributes->>'group_id'), occurred_at DESC);`, - `CREATE INDEX idx_journal_default_client_filter ON journal(operation, (attributes->>'id'), (attributes->>'client_id'), occurred_at DESC);`, - `CREATE INDEX idx_journal_default_channel_filter ON journal(operation, (attributes->>'id'), (attributes->>'channel_id'), occurred_at DESC);`, - `CREATE TABLE IF NOT EXISTS clients_telemetry ( - client_id VARCHAR(36) PRIMARY KEY, - domain_id VARCHAR(36) NOT NULL, - inbound_messages BIGINT DEFAULT 0, - outbound_messages BIGINT DEFAULT 0, - first_seen TIMESTAMP, - last_seen TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS subscriptions ( - id VARCHAR(36) PRIMARY KEY, - subscriber_id VARCHAR(1024) NOT NULL, - channel_id VARCHAR(36) NOT NULL, - subtopic VARCHAR(1024), - client_id VARCHAR(36), - FOREIGN KEY (client_id) REFERENCES clients_telemetry(client_id) ON DELETE CASCADE ON UPDATE CASCADE - )`, - }, - Down: []string{ - `DROP TABLE IF EXISTS clients_telemetry`, - `DROP TABLE IF EXISTS subscriptions`, - `DROP TABLE IF EXISTS journal`, - }, - }, - { - Id: "journal_02", - Up: []string{ - `CREATE TABLE IF NOT EXISTS clients_telemetry ( - client_id VARCHAR(36) PRIMARY KEY, - domain_id VARCHAR(36) NOT NULL, - inbound_messages BIGINT DEFAULT 0, - outbound_messages BIGINT DEFAULT 0, - first_seen TIMESTAMP, - last_seen TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS subscriptions ( - id VARCHAR(36) PRIMARY KEY, - subscriber_id VARCHAR(1024) NOT NULL, - channel_id VARCHAR(36) NOT NULL, - subtopic VARCHAR(1024), - client_id VARCHAR(36), - FOREIGN KEY (client_id) REFERENCES clients_telemetry(client_id) ON DELETE CASCADE ON UPDATE CASCADE - )`, - `ALTER TABLE journal ALTER COLUMN occurred_at TYPE TIMESTAMPTZ;`, - `ALTER TABLE clients_telemetry ALTER COLUMN first_seen TYPE TIMESTAMPTZ;`, - `ALTER TABLE clients_telemetry ALTER COLUMN last_seen TYPE TIMESTAMPTZ;`, - }, - Down: []string{ - `ALTER TABLE journal ALTER COLUMN occurred_at TYPE TIMESTAMP;`, - `ALTER TABLE clients_telemetry ALTER COLUMN first_seen TYPE TIMESTAMP;`, - `ALTER TABLE clients_telemetry ALTER COLUMN last_seen TYPE TIMESTAMP;`, - }, - }, - { - Id: "journal_03", - Up: []string{ - `CREATE INDEX IF NOT EXISTS idx_journal_domain ON journal(domain);`, - }, - Down: []string{ - `DROP INDEX IF EXISTS idx_journal_domain;`, - }, - }, - }, - } -} diff --git a/journal/postgres/journal.go b/journal/postgres/journal.go deleted file mode 100644 index b3352c930..000000000 --- a/journal/postgres/journal.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/postgres" -) - -type repository struct { - db postgres.Database - eh errors.Handler -} - -func NewRepository(db postgres.Database) journal.Repository { - errHandlerOptions := []errors.HandlerOption{ - postgres.WithDuplicateErrors(NewDuplicateErrors()), - } - return &repository{ - db: db, - eh: postgres.NewErrorHandler(errHandlerOptions...), - } -} - -func (repo *repository) Save(ctx context.Context, j journal.Journal) (err error) { - domain, ok := j.Attributes["domain"].(string) - if ok { - j.Domain = domain - } - if strings.HasPrefix(j.Operation, "domain.") { - domain, ok := j.Attributes["id"].(string) - if ok { - j.Domain = domain - } - } - - q := `INSERT INTO journal (id, operation, occurred_at, attributes, metadata, domain) - VALUES (:id, :operation, :occurred_at, :attributes, :metadata, :domain);` - - dbJournal, err := toDBJournal(j) - if err != nil { - return repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - - if _, err = repo.db.NamedExecContext(ctx, q, dbJournal); err != nil { - return repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - - return nil -} - -func (repo *repository) RetrieveAll(ctx context.Context, page journal.Page) (journal.JournalsPage, error) { - query := pageQuery(page) - - sq := "operation, occurred_at, domain" - if page.WithAttributes { - sq += ", attributes" - } - if page.WithMetadata { - sq += ", metadata" - } - if page.Direction == "" { - page.Direction = "ASC" - } - q := fmt.Sprintf("SELECT %s, COUNT(*) OVER() AS total_count FROM journal %s ORDER BY occurred_at %s LIMIT :limit OFFSET :offset;", sq, query, page.Direction) - - rows, err := repo.db.NamedQueryContext(ctx, q, page) - if err != nil { - return journal.JournalsPage{}, repo.eh.HandleError(repoerr.ErrViewEntity, err) - } - defer rows.Close() - - var total uint64 - var items []journal.Journal - for rows.Next() { - var item dbJournal - if err = rows.StructScan(&item); err != nil { - return journal.JournalsPage{}, repo.eh.HandleError(repoerr.ErrViewEntity, err) - } - total = item.TotalCount - j, err := toJournal(item) - if err != nil { - return journal.JournalsPage{}, err - } - items = append(items, j) - } - - if len(items) == 0 { - tq := fmt.Sprintf(`SELECT COUNT(*) FROM journal %s;`, query) - total, err = postgres.Total(ctx, repo.db, tq, page) - if err != nil { - return journal.JournalsPage{}, repo.eh.HandleError(repoerr.ErrViewEntity, err) - } - } - - return journal.JournalsPage{ - Total: total, - Offset: page.Offset, - Limit: page.Limit, - Journals: items, - }, nil -} - -func pageQuery(pm journal.Page) string { - var query []string - var emq string - if pm.Operation != "" { - query = append(query, "operation = :operation") - } - if !pm.From.IsZero() { - query = append(query, "occurred_at >= :from") - } - if !pm.To.IsZero() { - query = append(query, "occurred_at <= :to") - } - if pm.EntityID != "" { - query = append(query, pm.EntityType.Query()) - } - - if len(query) > 0 { - emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - - return emq -} - -type dbJournal struct { - ID string `db:"id"` - Operation string `db:"operation"` - Domain string `db:"domain"` - OccurredAt time.Time `db:"occurred_at"` - Attributes []byte `db:"attributes"` - Metadata []byte `db:"metadata"` - TotalCount uint64 `db:"total_count"` -} - -func toDBJournal(j journal.Journal) (dbJournal, error) { - if j.OccurredAt.IsZero() { - j.OccurredAt = time.Now().UTC() - } - - attributes := []byte("{}") - if len(j.Attributes) > 0 { - b, err := json.Marshal(j.Attributes) - if err != nil { - return dbJournal{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - attributes = b - } - - metadata := []byte("{}") - if len(j.Metadata) > 0 { - b, err := json.Marshal(j.Metadata) - if err != nil { - return dbJournal{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - metadata = b - } - - return dbJournal{ - ID: j.ID, - Operation: j.Operation, - Domain: j.Domain, - OccurredAt: j.OccurredAt, - Attributes: attributes, - Metadata: metadata, - }, nil -} - -func toJournal(dbj dbJournal) (journal.Journal, error) { - var attributes map[string]any - if dbj.Attributes != nil { - if err := json.Unmarshal(dbj.Attributes, &attributes); err != nil { - return journal.Journal{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - } - - var metadata map[string]any - if dbj.Metadata != nil { - if err := json.Unmarshal(dbj.Metadata, &metadata); err != nil { - return journal.Journal{}, errors.Wrap(repoerr.ErrMalformedEntity, err) - } - } - - return journal.Journal{ - Operation: dbj.Operation, - Domain: dbj.Domain, - OccurredAt: dbj.OccurredAt.UTC(), - Attributes: attributes, - Metadata: metadata, - }, nil -} diff --git a/journal/postgres/journal_test.go b/journal/postgres/journal_test.go deleted file mode 100644 index ce9a3a4c7..000000000 --- a/journal/postgres/journal_test.go +++ /dev/null @@ -1,1332 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "math/rand" - "sort" - "strings" - "testing" - "time" - - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/journal/postgres" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - operation = "user.create" - payload = map[string]any{ - "temperature": rand.Float64(), - "humidity": float64(rand.Intn(1000)), - "locations": []any{ - strings.Repeat("a", 100), - strings.Repeat("a", 100), - }, - "status": "active", - "nested": map[string]any{ - "nested": map[string]any{ - "nested": map[string]any{ - "nested": map[string]any{ - "key": "value", - }, - }, - }, - }, - } - - entityID = testsutil.GenerateUUID(&testing.T{}) - domain = testsutil.GenerateUUID(&testing.T{}) - clientOperation = "client.create" - channelOperation = "channel.create" - groupOperation = "group.create" - clientAttributesV1 = map[string]any{ - "id": entityID, - "status": "enabled", - "created_at": time.Now().Add(-time.Hour), - "name": "client", - "tags": []any{"tag1", "tag2"}, - "domain": domain, - "metadata": payload, - "identity": testsutil.GenerateUUID(&testing.T{}), - } - clientAttributesV2 = map[string]any{ - "entity_id": entityID, - "metadata": payload, - } - userAttributesV1 = map[string]any{ - "id": entityID, - "status": "enabled", - "created_at": time.Now().Add(-time.Hour), - "name": "user", - "tags": []any{"tag1", "tag2"}, - "domain": domain, - "metadata": payload, - "identity": testsutil.GenerateUUID(&testing.T{}), - } - userAttributesV2 = map[string]any{ - "user_id": entityID, - "metadata": payload, - } - channelAtttributes = map[string]any{ - "id": entityID, - "status": "enabled", - "created_at": time.Now().Add(-time.Hour), - "name": "channel", - } - groupAttributes = map[string]any{ - "id": entityID, - "status": "enabled", - "created_at": time.Now().Add(-time.Hour), - "name": "group", - } - validTimeStamp = time.Now().UTC().Truncate(time.Millisecond) - errJournalExists = errors.NewRequestError("journal entry already exists") -) - -func TestJournalSave(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM journal") - require.Nil(t, err, fmt.Sprintf("clean journal unexpected error: %s", err)) - }) - repo := postgres.NewRepository(database) - - occurredAt := time.Now() - id := testsutil.GenerateUUID(t) - - cases := []struct { - desc string - journal journal.Journal - err error - }{ - { - desc: "new journal successfully", - journal: journal.Journal{ - ID: id, - Operation: operation, - OccurredAt: occurredAt, - Attributes: payload, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with duplicate journal", - journal: journal.Journal{ - ID: id, - Operation: operation, - OccurredAt: occurredAt, - Attributes: payload, - Metadata: payload, - }, - err: errJournalExists, - }, - { - desc: "with massive journal metadata and attributes", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation, - OccurredAt: time.Now(), - Attributes: map[string]any{ - "attributes": map[string]any{ - "attributes": map[string]any{ - "attributes": map[string]any{ - "attributes": map[string]any{ - "attributes": map[string]any{ - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - Metadata: map[string]any{ - "metadata": map[string]any{ - "metadata": map[string]any{ - "metadata": map[string]any{ - "metadata": map[string]any{ - "metadata": map[string]any{ - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - "data": payload, - }, - }, - err: nil, - }, - { - desc: "with nil journal operation", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - OccurredAt: time.Now(), - Attributes: payload, - Metadata: payload, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "with empty journal operation", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: "", - OccurredAt: time.Now().Add(-time.Hour), - Attributes: payload, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with nil journal occurred_at", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation, - Attributes: payload, - Metadata: payload, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "with empty journal occurred_at", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation, - OccurredAt: time.Time{}, - Attributes: payload, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with nil journal attributes", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation + ".with.nil.attributes", - OccurredAt: time.Now(), - Metadata: payload, - }, - err: nil, - }, - { - desc: "with invalid journal attributes", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation, - OccurredAt: time.Now(), - Attributes: map[string]any{"invalid": make(chan struct{})}, - Metadata: payload, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "with empty journal attributes", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation + ".with.empty.attributes", - OccurredAt: time.Now(), - Attributes: map[string]any{}, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with nil journal metadata", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation + ".with.nil.metadata", - OccurredAt: time.Now(), - Attributes: payload, - }, - err: nil, - }, - { - desc: "with invalid journal metadata", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation, - OccurredAt: time.Now(), - Metadata: map[string]any{"invalid": make(chan struct{})}, - Attributes: payload, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "with empty journal metadata", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation + ".with.empty.metadata", - OccurredAt: time.Now(), - Metadata: map[string]any{}, - Attributes: payload, - }, - err: nil, - }, - { - desc: "with domain in attributes", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: operation + ".with.domain.in.attributes", - OccurredAt: time.Now(), - Attributes: map[string]any{ - "domain": testsutil.GenerateUUID(t), - "data": "test", - }, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with domain operation prefix", - journal: journal.Journal{ - ID: testsutil.GenerateUUID(t), - Operation: "domain.create", - OccurredAt: time.Now(), - Attributes: map[string]any{ - "id": testsutil.GenerateUUID(t), - "name": "test-domain", - "status": "enabled", - }, - Metadata: payload, - }, - err: nil, - }, - { - desc: "with empty journal", - journal: journal.Journal{}, - err: repoerr.ErrCreateEntity, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - switch err := repo.Save(context.Background(), tc.journal); { - case err == nil: - assert.Nil(t, err) - default: - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } - }) - } -} - -func TestJournalRetrieveAll(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM journal") - require.Nil(t, err, fmt.Sprintf("clean journal unexpected error: %s", err)) - }) - repo := postgres.NewRepository(database) - - num := 200 - - var items []journal.Journal - for i := 0; i < num; i++ { - j := journal.Journal{ - ID: testsutil.GenerateUUID(t), - Domain: domain, - Operation: fmt.Sprintf("%s-%d", operation, i), - OccurredAt: time.Now().UTC().Truncate(time.Microsecond), - Attributes: userAttributesV1, - Metadata: payload, - } - if i%2 == 0 { - j.Operation = fmt.Sprintf("%s-%d", clientOperation, i) - j.Attributes = clientAttributesV1 - } - if i%3 == 0 { - j.Attributes = userAttributesV2 - } - if i%5 == 0 { - j.Attributes = clientAttributesV2 - } - if i%13 == 0 { - j.Operation = fmt.Sprintf("%s-%d", channelOperation, i) - j.Attributes = channelAtttributes - } - if i%17 == 0 { - j.Operation = fmt.Sprintf("%s-%d", groupOperation, i) - j.Attributes = groupAttributes - } - err := repo.Save(context.Background(), j) - require.Nil(t, err, fmt.Sprintf("create journal unexpected error: %s", err)) - j.ID = "" - items = append(items, j) - } - - reversedItems := make([]journal.Journal, len(items)) - copy(reversedItems, items) - sort.Slice(reversedItems, func(i, j int) bool { - return reversedItems[i].OccurredAt.After(reversedItems[j].OccurredAt) - }) - - cases := []struct { - desc string - page journal.Page - response journal.JournalsPage - err error - }{ - { - desc: "successfully", - page: journal.Page{ - Offset: 0, - Limit: 1, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 1, - Journals: items[:1], - }, - err: nil, - }, - { - desc: "with offset and empty limit", - page: journal.Page{ - Offset: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 10, - Limit: 0, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with limit and empty offset", - page: journal.Page{ - Limit: 50, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 50, - Journals: items[:50], - }, - }, - { - desc: "with offset and limit", - page: journal.Page{ - Offset: 10, - Limit: 50, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 10, - Limit: 50, - Journals: items[10:60], - }, - }, - { - desc: "with offset out of range", - page: journal.Page{ - Offset: 1000, - Limit: 50, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 1000, - Limit: 50, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with offset and limit out of range", - page: journal.Page{ - Offset: 170, - Limit: 50, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 170, - Limit: 50, - Journals: items[170:200], - }, - }, - { - desc: "with limit out of range", - page: journal.Page{ - Offset: 0, - Limit: 1000, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 1000, - Journals: items, - }, - }, - { - desc: "with empty page", - page: journal.Page{}, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 0, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with operation", - page: journal.Page{ - Operation: items[0].Operation, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Journals: []journal.Journal{items[0]}, - }, - }, - { - desc: "with invalid operation", - page: journal.Page{ - Operation: strings.Repeat("a", 37), - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: 0, - Offset: 0, - Limit: 10, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with attributes", - page: journal.Page{ - WithAttributes: true, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with metadata", - page: journal.Page{ - WithMetadata: true, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with attributes and Metadata", - page: journal.Page{ - WithAttributes: true, - WithMetadata: true, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with from", - page: journal.Page{ - From: items[0].OccurredAt, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with invalid from", - page: journal.Page{ - From: time.Now().UTC().Truncate(time.Millisecond).Add(time.Hour), - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: 0, - Offset: 0, - Limit: 10, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with to", - page: journal.Page{ - To: items[num-1].OccurredAt, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with invalid to", - page: journal.Page{ - To: time.Now().UTC().Truncate(time.Millisecond).Add(-time.Hour), - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: 0, - Offset: 0, - Limit: 10, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with from and to", - page: journal.Page{ - From: items[0].OccurredAt, - To: items[num-1].OccurredAt, - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with asc direction", - page: journal.Page{ - Direction: "ASC", - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: items[:10], - }, - }, - { - desc: "with desc direction", - page: journal.Page{ - Direction: "DESC", - Offset: 0, - Limit: 10, - }, - response: journal.JournalsPage{ - Total: uint64(num), - Offset: 0, - Limit: 10, - Journals: reversedItems[:10], - }, - }, - { - desc: "with user entity type", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: entityID, - EntityType: journal.UserEntity, - }, - response: journal.JournalsPage{ - Total: uint64(len(extractEntities(items, journal.UserEntity, entityID))), - Offset: 0, - Limit: 10, - Journals: extractEntities(items, journal.UserEntity, entityID)[:10], - }, - }, - { - desc: "with user entity type, attributes and metadata", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: entityID, - EntityType: journal.UserEntity, - WithAttributes: true, - WithMetadata: true, - }, - response: journal.JournalsPage{ - Total: uint64(len(extractEntities(items, journal.UserEntity, entityID))), - Offset: 0, - Limit: 10, - Journals: extractEntities(items, journal.UserEntity, entityID)[:10], - }, - }, - { - desc: "with client entity type", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: entityID, - EntityType: journal.ClientEntity, - }, - response: journal.JournalsPage{ - Total: uint64(len(extractEntities(items, journal.ClientEntity, entityID))), - Offset: 0, - Limit: 10, - Journals: extractEntities(items, journal.ClientEntity, entityID)[:10], - }, - }, - { - desc: "with channel entity type", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: entityID, - EntityType: journal.ChannelEntity, - }, - response: journal.JournalsPage{ - Total: uint64(len(extractEntities(items, journal.ChannelEntity, entityID))), - Offset: 0, - Limit: 10, - Journals: extractEntities(items, journal.ChannelEntity, entityID)[:10], - }, - }, - { - desc: "with group entity type", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: entityID, - EntityType: journal.GroupEntity, - }, - response: journal.JournalsPage{ - Total: uint64(len(extractEntities(items, journal.GroupEntity, entityID))), - Offset: 0, - Limit: 10, - Journals: extractEntities(items, journal.GroupEntity, entityID)[:10], - }, - }, - { - desc: "with invalid entity id", - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: testsutil.GenerateUUID(&testing.T{}), - EntityType: journal.ChannelEntity, - }, - response: journal.JournalsPage{ - Total: 0, - Offset: 0, - Limit: 10, - Journals: []journal.Journal(nil), - }, - }, - { - desc: "with all filters", - page: journal.Page{ - Offset: 0, - Limit: 10, - Operation: items[0].Operation, - From: items[0].OccurredAt, - To: items[num-1].OccurredAt, - WithAttributes: true, - WithMetadata: true, - Direction: "asc", - }, - response: journal.JournalsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Journals: []journal.Journal{items[0]}, - }, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - page, err := repo.RetrieveAll(context.Background(), tc.page) - assert.Equal(t, tc.response.Total, page.Total) - assert.Equal(t, tc.response.Offset, page.Offset) - assert.Equal(t, tc.response.Limit, page.Limit) - for i := range tc.response.Journals { - tc.response.Journals[i].Attributes = map[string]any{} - page.Journals[i].Attributes = map[string]any{} - tc.response.Journals[i].Metadata = map[string]any{} - page.Journals[i].Metadata = map[string]any{} - tc.response.Journals[i].OccurredAt = validTimeStamp - page.Journals[i].OccurredAt = validTimeStamp - } - assert.ElementsMatch(t, tc.response.Journals, page.Journals) - - assert.Equal(t, tc.err, err) - }) - } -} - -func extractEntities(journals []journal.Journal, entityType journal.EntityType, entityID string) []journal.Journal { - var entities []journal.Journal - for _, j := range journals { - switch entityType { - case journal.UserEntity: - if strings.HasPrefix(j.Operation, "user.") && j.Attributes["id"] == entityID || j.Attributes["user_id"] == entityID { - entities = append(entities, j) - } - case journal.GroupEntity: - if strings.HasPrefix(j.Operation, "group.") && (j.Attributes["id"] == entityID || j.Attributes["entity_id"] == entityID) { - entities = append(entities, j) - } - case journal.ClientEntity: - if strings.HasPrefix(j.Operation, "client.") && (j.Attributes["id"] == entityID || j.Attributes["entity_id"] == entityID) { - entities = append(entities, j) - } - case journal.ChannelEntity: - if strings.HasPrefix(j.Operation, "channel.") && (j.Attributes["id"] == entityID || j.Attributes["entity_id"] == entityID) { - entities = append(entities, j) - } - } - } - - return entities -} - -func TestSaveClientTelemetry(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err, fmt.Sprintf("clean clients_telemetry unexpected error: %s", err)) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - firstSeen := time.Now().UTC().Truncate(time.Millisecond) - lastSeen := time.Now().UTC().Add(time.Hour).Truncate(time.Millisecond) - - cases := []struct { - desc string - telemetry journal.ClientTelemetry - err error - }{ - { - desc: "save client telemetry successfully", - telemetry: journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - InboundMessages: 10, - OutboundMessages: 5, - FirstSeen: firstSeen, - LastSeen: lastSeen, - }, - err: nil, - }, - { - desc: "save duplicate client telemetry", - telemetry: journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - InboundMessages: 20, - OutboundMessages: 10, - FirstSeen: firstSeen, - LastSeen: lastSeen, - }, - err: repoerr.ErrConflict, - }, - { - desc: "save client telemetry with zero messages", - telemetry: journal.ClientTelemetry{ - ClientID: testsutil.GenerateUUID(t), - DomainID: domainID, - InboundMessages: 0, - OutboundMessages: 0, - FirstSeen: firstSeen, - LastSeen: time.Time{}, - }, - err: nil, - }, - { - desc: "save client telemetry with high message counts", - telemetry: journal.ClientTelemetry{ - ClientID: testsutil.GenerateUUID(t), - DomainID: testsutil.GenerateUUID(t), - InboundMessages: 1000000, - OutboundMessages: 999999, - FirstSeen: firstSeen, - LastSeen: lastSeen, - }, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.SaveClientTelemetry(context.Background(), tc.telemetry) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - }) - } -} - -func TestDeleteClientTelemetry(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err, fmt.Sprintf("clean clients_telemetry unexpected error: %s", err)) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - - ct := journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - InboundMessages: 10, - OutboundMessages: 5, - FirstSeen: time.Now().UTC(), - LastSeen: time.Now().UTC(), - } - - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - cases := []struct { - desc string - clientID string - domainID string - err error - }{ - { - desc: "delete existing client telemetry", - clientID: clientID, - domainID: domainID, - err: nil, - }, - { - desc: "delete non-existing client telemetry", - clientID: testsutil.GenerateUUID(t), - domainID: domainID, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.DeleteClientTelemetry(context.Background(), tc.clientID, tc.domainID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - }) - } -} - -func TestRetrieveClientTelemetry(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err, fmt.Sprintf("clean clients_telemetry unexpected error: %s", err)) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - firstSeen := time.Now().UTC().Truncate(time.Millisecond) - lastSeen := time.Now().UTC().Add(time.Hour).Truncate(time.Millisecond) - - ct := journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - InboundMessages: 10, - OutboundMessages: 5, - FirstSeen: firstSeen, - LastSeen: lastSeen, - } - - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - cases := []struct { - desc string - clientID string - domainID string - response journal.ClientTelemetry - err error - }{ - { - desc: "retrieve existing client telemetry", - clientID: clientID, - domainID: domainID, - response: ct, - err: nil, - }, - { - desc: "retrieve non-existing client telemetry", - clientID: testsutil.GenerateUUID(t), - domainID: domainID, - response: journal.ClientTelemetry{}, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - result, err := repo.RetrieveClientTelemetry(context.Background(), tc.clientID, tc.domainID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.response.ClientID, result.ClientID) - assert.Equal(t, tc.response.DomainID, result.DomainID) - assert.Equal(t, tc.response.InboundMessages, result.InboundMessages) - assert.Equal(t, tc.response.OutboundMessages, result.OutboundMessages) - assert.Equal(t, tc.response.FirstSeen.Unix(), result.FirstSeen.Unix()) - assert.Equal(t, tc.response.LastSeen.Unix(), result.LastSeen.Unix()) - } - }) - } -} - -func TestAddSubscription(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM subscriptions") - require.Nil(t, err) - _, err = db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - - ct := journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - FirstSeen: time.Now().UTC(), - } - - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - cases := []struct { - desc string - subscription journal.ClientSubscription - err error - }{ - { - desc: "add subscription successfully", - subscription: journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - ClientID: clientID, - }, - err: nil, - }, - { - desc: "add subscription with empty subtopic", - subscription: journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - Subtopic: "", - ClientID: clientID, - }, - err: nil, - }, - { - desc: "add duplicate subscription", - subscription: journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - Subtopic: "another-subtopic", - ClientID: clientID, - }, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.AddSubscription(context.Background(), tc.subscription) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - }) - } -} - -func TestCountSubscriptions(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM subscriptions") - require.Nil(t, err) - _, err = db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - - ct := journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - FirstSeen: time.Now().UTC(), - } - - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - for i := 0; i < 3; i++ { - sub := journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: testsutil.GenerateUUID(t), - ChannelID: testsutil.GenerateUUID(t), - Subtopic: fmt.Sprintf("subtopic%d", i), - ClientID: clientID, - } - err := repo.AddSubscription(context.Background(), sub) - require.Nil(t, err) - } - - cases := []struct { - desc string - clientID string - count uint64 - err error - }{ - { - desc: "count subscriptions for existing client", - clientID: clientID, - count: 3, - err: nil, - }, - { - desc: "count subscriptions for non-existing client", - clientID: testsutil.GenerateUUID(t), - count: 0, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - count, err := repo.CountSubscriptions(context.Background(), tc.clientID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - assert.Equal(t, tc.count, count) - }) - } -} - -func TestRemoveSubscription(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM subscriptions") - require.Nil(t, err) - _, err = db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - subscriberID := testsutil.GenerateUUID(t) - - ct := journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - FirstSeen: time.Now().UTC(), - } - - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - sub := journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: subscriberID, - ChannelID: testsutil.GenerateUUID(t), - Subtopic: "subtopic", - ClientID: clientID, - } - - err = repo.AddSubscription(context.Background(), sub) - require.Nil(t, err) - - cases := []struct { - desc string - subscriberID string - err error - }{ - { - desc: "remove existing subscription", - subscriberID: subscriberID, - err: nil, - }, - { - desc: "remove non-existing subscription", - subscriberID: testsutil.GenerateUUID(t), - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.RemoveSubscription(context.Background(), tc.subscriberID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - }) - } -} - -func TestIncrementInboundMessages(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err) - }) - repo := postgres.NewRepository(database) - - clientID := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - firstSeen := time.Now().UTC().Truncate(time.Millisecond) - - cases := []struct { - desc string - telemetry journal.ClientTelemetry - expectedInbound uint64 - err error - setupExisting bool - existingInbound uint64 - }{ - { - desc: "increment inbound messages for new client", - telemetry: journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - FirstSeen: firstSeen, - LastSeen: firstSeen, - }, - expectedInbound: 1, - setupExisting: false, - err: nil, - }, - { - desc: "increment inbound messages for existing client", - telemetry: journal.ClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - FirstSeen: firstSeen, - LastSeen: firstSeen.Add(time.Hour), - }, - expectedInbound: 2, - setupExisting: true, - existingInbound: 1, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.IncrementInboundMessages(context.Background(), tc.telemetry) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - - if err == nil { - result, err := repo.RetrieveClientTelemetry(context.Background(), tc.telemetry.ClientID, tc.telemetry.DomainID) - require.Nil(t, err) - assert.Equal(t, tc.expectedInbound, result.InboundMessages) - } - }) - } -} - -func TestIncrementOutboundMessages(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM subscriptions") - require.Nil(t, err) - _, err = db.Exec("DELETE FROM clients_telemetry") - require.Nil(t, err) - }) - repo := postgres.NewRepository(database) - - clientID1 := testsutil.GenerateUUID(t) - clientID2 := testsutil.GenerateUUID(t) - domainID := testsutil.GenerateUUID(t) - channelID := testsutil.GenerateUUID(t) - subtopic := "test/subtopic" - - for i, cid := range []string{clientID1, clientID2} { - ct := journal.ClientTelemetry{ - ClientID: cid, - DomainID: domainID, - FirstSeen: time.Now().UTC(), - } - err := repo.SaveClientTelemetry(context.Background(), ct) - require.Nil(t, err) - - for j := 0; j < 2; j++ { - sub := journal.ClientSubscription{ - ID: testsutil.GenerateUUID(t), - SubscriberID: fmt.Sprintf("subscriber-%d-%d", i, j), - ChannelID: channelID, - Subtopic: subtopic, - ClientID: cid, - } - err = repo.AddSubscription(context.Background(), sub) - require.Nil(t, err) - } - } - - cases := []struct { - desc string - channelID string - subtopic string - expectedIncrement uint64 - setupAdditional bool - err error - }{ - { - desc: "increment outbound messages for subscribed clients with multiple subscriptions", - channelID: channelID, - subtopic: subtopic, - expectedIncrement: 2, - err: nil, - }, - { - desc: "increment for non-existing channel", - channelID: testsutil.GenerateUUID(t), - subtopic: subtopic, - expectedIncrement: 0, - err: nil, - }, - { - desc: "increment with different subtopic", - channelID: channelID, - subtopic: "different/subtopic", - expectedIncrement: 0, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.IncrementOutboundMessages(context.Background(), tc.channelID, tc.subtopic) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err)) - - if err == nil && tc.expectedIncrement > 0 { - for _, cid := range []string{clientID1, clientID2} { - result, err := repo.RetrieveClientTelemetry(context.Background(), cid, domainID) - require.Nil(t, err) - assert.Equal(t, tc.expectedIncrement, result.OutboundMessages) - } - } - }) - } -} diff --git a/journal/postgres/setup_test.go b/journal/postgres/setup_test.go deleted file mode 100644 index bb9a1307a..000000000 --- a/journal/postgres/setup_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "database/sql" - "fmt" - "log" - "os" - "testing" - "time" - - jpostgres "github.com/absmach/magistrala/journal/postgres" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/jmoiron/sqlx" - dockertest "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "go.opentelemetry.io/otel" -) - -var ( - db *sqlx.DB - database postgres.Database - tracer = otel.Tracer("repo_tests") -) - -func TestMain(m *testing.M) { - pool, err := dockertest.NewPool("") - if err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - container, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "postgres", - Tag: "16.2-alpine", - Env: []string{ - "POSTGRES_USER=test", - "POSTGRES_PASSWORD=test", - "POSTGRES_DB=test", - "listen_addresses = '*'", - }, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - if err != nil { - log.Fatalf("Could not start container: %s", err) - } - - port := container.GetPort("5432/tcp") - - // exponential backoff-retry, because the application in the container might not be ready to accept connections yet - pool.MaxWait = 120 * time.Second - if err := pool.Retry(func() error { - url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port) - db, err := sql.Open("pgx", url) - if err != nil { - return err - } - return db.Ping() - }); err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - dbConfig := postgres.Config{ - Host: "localhost", - Port: port, - User: "test", - Pass: "test", - Name: "test", - SSLMode: "disable", - SSLCert: "", - SSLKey: "", - SSLRootCert: "", - } - - if db, err = postgres.Setup(dbConfig, *jpostgres.Migration()); err != nil { - log.Fatalf("Could not setup test DB connection: %s", err) - } - - database = postgres.NewDatabase(db, dbConfig, tracer) - - code := m.Run() - - // Defers will not be run when using os.Exit - db.Close() - if err := pool.Purge(container); err != nil { - log.Fatalf("Could not purge container: %s", err) - } - - os.Exit(code) -} diff --git a/journal/postgres/telemetry.go b/journal/postgres/telemetry.go deleted file mode 100644 index 2ce33c5ed..000000000 --- a/journal/postgres/telemetry.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/jackc/pgtype" -) - -func (repo *repository) SaveClientTelemetry(ctx context.Context, ct journal.ClientTelemetry) error { - q := `INSERT INTO clients_telemetry (client_id, domain_id, inbound_messages, outbound_messages, first_seen, last_seen) - VALUES (:client_id, :domain_id, :inbound_messages, :outbound_messages, :first_seen, :last_seen);` - - dbct, err := toDBClientsTelemetry(ct) - if err != nil { - return errors.Wrap(repoerr.ErrCreateEntity, err) - } - - if _, err := repo.db.NamedExecContext(ctx, q, dbct); err != nil { - return postgres.HandleError(repoerr.ErrCreateEntity, err) - } - - return nil -} - -func (repo *repository) DeleteClientTelemetry(ctx context.Context, clientID, domainID string) error { - q := `DELETE FROM clients_telemetry AS ct WHERE ct.client_id = :client_id AND ct.domain_id = :domain_id;` - - dbct := dbClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - } - - result, err := repo.db.NamedExecContext(ctx, q, dbct) - if err != nil { - return postgres.HandleError(repoerr.ErrRemoveEntity, err) - } - if rows, _ := result.RowsAffected(); rows == 0 { - return repoerr.ErrNotFound - } - - return nil -} - -func (repo *repository) RetrieveClientTelemetry(ctx context.Context, clientID, domainID string) (journal.ClientTelemetry, error) { - q := `SELECT * FROM clients_telemetry WHERE client_id = :client_id AND domain_id = :domain_id;` - - dbct := dbClientTelemetry{ - ClientID: clientID, - DomainID: domainID, - } - - rows, err := repo.db.NamedQueryContext(ctx, q, dbct) - if err != nil { - return journal.ClientTelemetry{}, postgres.HandleError(repoerr.ErrViewEntity, err) - } - defer rows.Close() - - dbct = dbClientTelemetry{} - if rows.Next() { - if err = rows.StructScan(&dbct); err != nil { - return journal.ClientTelemetry{}, postgres.HandleError(repoerr.ErrViewEntity, err) - } - - ct, err := toClientsTelemetry(dbct) - if err != nil { - return journal.ClientTelemetry{}, errors.Wrap(repoerr.ErrFailedOpDB, err) - } - - return ct, nil - } - - return journal.ClientTelemetry{}, repoerr.ErrNotFound -} - -func (repo *repository) AddSubscription(ctx context.Context, sub journal.ClientSubscription) error { - q := ` - INSERT INTO subscriptions (id, subscriber_id, channel_id, subtopic, client_id) - SELECT :id, :subscriber_id, :channel_id, :subtopic, :client_id - FROM clients_telemetry - WHERE client_id = :client_id - RETURNING id; - ` - - _, err := repo.db.NamedExecContext(ctx, q, sub) - if err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - return nil -} - -func (repo *repository) CountSubscriptions(ctx context.Context, clientID string) (uint64, error) { - q := `SELECT COUNT(*) FROM subscriptions WHERE client_id = :client_id;` - - sb := journal.ClientSubscription{ - ClientID: clientID, - } - - total, err := postgres.Total(ctx, repo.db, q, sb) - if err != nil { - return 0, postgres.HandleError(repoerr.ErrViewEntity, err) - } - - return total, nil -} - -func (repo *repository) RemoveSubscription(ctx context.Context, subscriberID string) error { - q := `DELETE FROM subscriptions WHERE subscriber_id = :subscriber_id;` - - sb := journal.ClientSubscription{ - SubscriberID: subscriberID, - } - - _, err := repo.db.NamedExecContext(ctx, q, sb) - if err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - return nil -} - -func (repo *repository) IncrementInboundMessages(ctx context.Context, ct journal.ClientTelemetry) error { - q := `INSERT INTO clients_telemetry (client_id,domain_id, inbound_messages,first_seen, last_seen) - VALUES (:client_id, :domain_id, 1, :first_seen, :last_seen) - ON CONFLICT (client_id) - DO UPDATE SET - inbound_messages = clients_telemetry.inbound_messages + 1, - last_seen = EXCLUDED.last_seen; - ` - - dbct, err := toDBClientsTelemetry(ct) - if err != nil { - return errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - result, err := repo.db.NamedExecContext(ctx, q, dbct) - if err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - if rows, _ := result.RowsAffected(); rows == 0 { - return repoerr.ErrNotFound - } - - return nil -} - -func (repo *repository) IncrementOutboundMessages(ctx context.Context, channelID, subtopic string) error { - query := ` - SELECT client_id, COUNT(*) AS match_count - FROM subscriptions - WHERE channel_id = :channel_id AND subtopic = :subtopic - GROUP BY client_id - ` - sb := journal.ClientSubscription{ - ChannelID: channelID, - Subtopic: subtopic, - } - - rows, err := repo.db.NamedQueryContext(ctx, query, sb) - if err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer rows.Close() - - tx, err := repo.db.BeginTxx(ctx, nil) - if err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - q := `UPDATE clients_telemetry - SET outbound_messages = outbound_messages + $1 - WHERE client_id = $2; - ` - - for rows.Next() { - var clientID string - var count uint64 - if err = rows.Scan(&clientID, &count); err != nil { - if err := tx.Rollback(); err != nil { - return errors.Wrap(errors.ErrRollbackTx, err) - } - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - if _, err = repo.db.ExecContext(ctx, q, count, clientID); err != nil { - if err := tx.Rollback(); err != nil { - return errors.Wrap(errors.ErrRollbackTx, err) - } - return errors.Wrap(errors.ErrRollbackTx, err) - } - } - - if err = tx.Commit(); err != nil { - return postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - - return nil -} - -type dbClientTelemetry struct { - ClientID string `db:"client_id"` - DomainID string `db:"domain_id"` - InboundMessages uint64 `db:"inbound_messages"` - OutboundMessages uint64 `db:"outbound_messages"` - FirstSeen time.Time `db:"first_seen"` - LastSeen sql.NullTime `db:"last_seen"` -} - -func toDBClientsTelemetry(ct journal.ClientTelemetry) (dbClientTelemetry, error) { - var subs pgtype.TextArray - if err := subs.Set(ct.Subscriptions); err != nil { - return dbClientTelemetry{}, err - } - - var lastSeen sql.NullTime - if ct.LastSeen != (time.Time{}) { - lastSeen = sql.NullTime{Time: ct.LastSeen, Valid: true} - } - - return dbClientTelemetry{ - ClientID: ct.ClientID, - DomainID: ct.DomainID, - InboundMessages: ct.InboundMessages, - OutboundMessages: ct.OutboundMessages, - FirstSeen: ct.FirstSeen, - LastSeen: lastSeen, - }, nil -} - -func toClientsTelemetry(dbct dbClientTelemetry) (journal.ClientTelemetry, error) { - var lastSeen time.Time - if dbct.LastSeen.Valid { - lastSeen = dbct.LastSeen.Time - } - - return journal.ClientTelemetry{ - ClientID: dbct.ClientID, - DomainID: dbct.DomainID, - InboundMessages: dbct.InboundMessages, - OutboundMessages: dbct.OutboundMessages, - FirstSeen: dbct.FirstSeen, - LastSeen: lastSeen, - }, nil -} diff --git a/journal/service.go b/journal/service.go deleted file mode 100644 index 68ba09803..000000000 --- a/journal/service.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package journal - -import ( - "context" - "fmt" - "time" - - "github.com/absmach/magistrala" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/messaging" -) - -const ( - clientCreate = "client.create" - clientRemove = "client.remove" - mqttSubscribe = "mqtt.client_subscribe" - mqttDisconnect = "mqtt.client_disconnect" - messagingPublish = "messaging.client_publish" - messagingSubscribe = "messaging.client_subscribe" - messagingUnsubscribe = "messaging.client_unsubscribe" -) - -var ( - errSaveJournal = errors.New("failed to save journal") - errHandleTelemetry = errors.New("failed to handle client telemetry") - errInvalidSubTopic = errors.New("invalid subscribe topic") -) - -type service struct { - idProvider magistrala.IDProvider - repository Repository -} - -func NewService(idp magistrala.IDProvider, repository Repository) Service { - return &service{ - idProvider: idp, - repository: repository, - } -} - -func (svc *service) Save(ctx context.Context, journal Journal) error { - id, err := svc.idProvider.ID() - if err != nil { - return err - } - journal.ID = id - - if err := svc.repository.Save(ctx, journal); err != nil { - return errors.Wrap(errSaveJournal, err) - } - if err := svc.handleTelemetry(ctx, journal); err != nil { - return errors.Wrap(errHandleTelemetry, err) - } - - return nil -} - -func (svc *service) RetrieveAll(ctx context.Context, session smqauthn.Session, page Page) (JournalsPage, error) { - journalPage, err := svc.repository.RetrieveAll(ctx, page) - if err != nil { - return JournalsPage{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - return journalPage, nil -} - -func (svc *service) RetrieveClientTelemetry(ctx context.Context, session smqauthn.Session, clientID string) (ClientTelemetry, error) { - ct, err := svc.repository.RetrieveClientTelemetry(ctx, clientID, session.DomainID) - if err != nil { - return ClientTelemetry{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - subs, err := svc.repository.CountSubscriptions(ctx, clientID) - if err != nil { - return ClientTelemetry{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - ct.Subscriptions = subs - - return ct, nil -} - -func (svc *service) handleTelemetry(ctx context.Context, journal Journal) error { - switch journal.Operation { - case clientCreate: - return svc.addClientTelemetry(ctx, journal) - - case clientRemove: - return svc.removeClientTelemetry(ctx, journal) - - case mqttSubscribe: - return svc.addMqttSubscription(ctx, journal) - - case messagingSubscribe: - return svc.addSubscription(ctx, journal) - - case messagingUnsubscribe: - return svc.removeSubscription(ctx, journal) - - case messagingPublish: - return svc.updateMessageCount(ctx, journal) - - case mqttDisconnect: - return svc.removeMqttSubscription(ctx, journal) - - default: - return nil - } -} - -func (svc *service) addClientTelemetry(ctx context.Context, journal Journal) error { - ce, err := toClientEvent(journal, true) - if err != nil { - return err - } - ct := ClientTelemetry{ - ClientID: ce.id, - DomainID: ce.domain, - FirstSeen: ce.createdAt, - LastSeen: ce.createdAt, - } - return svc.repository.SaveClientTelemetry(ctx, ct) -} - -func (svc *service) removeClientTelemetry(ctx context.Context, journal Journal) error { - ce, err := toClientEvent(journal, false) - if err != nil { - return err - } - return svc.repository.DeleteClientTelemetry(ctx, ce.id, ce.domain) -} - -func (svc *service) addSubscription(ctx context.Context, journal Journal) error { - ae, err := toSubscribeEvent(journal) - if err != nil { - return err - } - channelID, subtopic, err := parseSubscriptionTopic(ae.topic) - if err != nil { - return err - } - - id, err := svc.idProvider.ID() - if err != nil { - return err - } - - sub := ClientSubscription{ - ID: id, - SubscriberID: ae.subscriberID, - ChannelID: channelID, - Subtopic: subtopic, - ClientID: ae.clientID, - } - - return svc.repository.AddSubscription(ctx, sub) -} - -func parseSubscriptionTopic(topic string) (string, string, error) { - _, channelID, subtopic, _, err := messaging.ParseSubscribeTopic(topic) - if err != nil { - return "", "", errors.Wrap(errInvalidSubTopic, err) - } - if channelID == "" { - return "", "", errInvalidSubTopic - } - return channelID, subtopic, nil -} - -func (svc *service) addMqttSubscription(ctx context.Context, journal Journal) error { - ae, err := toMqttSubscribeEvent(journal) - if err != nil { - return err - } - - id, err := svc.idProvider.ID() - if err != nil { - return err - } - - sub := ClientSubscription{ - ID: id, - SubscriberID: ae.subscriberID, - ChannelID: ae.channelID, - Subtopic: ae.subtopic, - ClientID: ae.clientID, - } - - return svc.repository.AddSubscription(ctx, sub) -} - -func (svc *service) removeSubscription(ctx context.Context, journal Journal) error { - ae, err := toUnsubscribeEvent(journal) - if err != nil { - return err - } - - return svc.repository.RemoveSubscription(ctx, ae.subscriberID) -} - -func (svc *service) removeMqttSubscription(ctx context.Context, journal Journal) error { - ae, err := toMqttDisconnectEvent(journal) - if err != nil { - return err - } - - return svc.repository.RemoveSubscription(ctx, ae.subscriberID) -} - -func (svc *service) updateMessageCount(ctx context.Context, journal Journal) error { - ae, err := toPublishEvent(journal) - if err != nil { - return err - } - ct := ClientTelemetry{ - ClientID: ae.clientID, - DomainID: ae.domainID, - FirstSeen: ae.occurredAt, - LastSeen: ae.occurredAt, - } - - if err := svc.repository.IncrementInboundMessages(ctx, ct); err != nil { - return err - } - if err := svc.repository.IncrementOutboundMessages(ctx, ae.channelID, ae.subtopic); err != nil { - return err - } - return nil -} - -type clientEvent struct { - id string - domain string - createdAt time.Time -} - -func toClientEvent(journal Journal, isCreate bool) (clientEvent, error) { - var createdAt time.Time - id, err := getStringAttribute(journal, "id") - if err != nil { - return clientEvent{}, err - } - domain, err := getStringAttribute(journal, "domain") - if err != nil { - return clientEvent{}, err - } - - if isCreate { - createdAtStr := journal.Attributes["created_at"].(string) - if createdAtStr != "" { - createdAt, err = time.Parse(time.RFC3339, createdAtStr) - if err != nil { - return clientEvent{}, fmt.Errorf("invalid created_at format") - } - } - } - return clientEvent{ - id: id, - domain: domain, - createdAt: createdAt, - }, nil -} - -type adapterEvent struct { - clientID string - channelID string - domainID string - subscriberID string - topic string - subtopic string - occurredAt time.Time -} - -func toPublishEvent(journal Journal) (adapterEvent, error) { - clientID, err := getStringAttribute(journal, "client_id") - if err != nil { - return adapterEvent{}, err - } - channelID, err := getStringAttribute(journal, "channel_id") - if err != nil { - return adapterEvent{}, err - } - domainID, err := getStringAttribute(journal, "domain_id") - if err != nil { - return adapterEvent{}, err - } - subtopic, err := getStringAttribute(journal, "subtopic") - if err != nil { - return adapterEvent{}, err - } - - return adapterEvent{ - clientID: clientID, - channelID: channelID, - domainID: domainID, - subtopic: subtopic, - occurredAt: journal.OccurredAt, - }, nil -} - -func toSubscribeEvent(journal Journal) (adapterEvent, error) { - subscriberID, err := getStringAttribute(journal, "subscriber_id") - if err != nil { - return adapterEvent{}, err - } - topic, err := getStringAttribute(journal, "topic") - if err != nil { - return adapterEvent{}, err - } - var clientID string - clientID, err = getStringAttribute(journal, "client_id") - if err != nil { - clientID = "" - } - - return adapterEvent{ - clientID: clientID, - subscriberID: subscriberID, - topic: topic, - }, nil -} - -func toUnsubscribeEvent(journal Journal) (adapterEvent, error) { - subscriberID, err := getStringAttribute(journal, "subscriber_id") - if err != nil { - return adapterEvent{}, err - } - topic, err := getStringAttribute(journal, "topic") - if err != nil { - return adapterEvent{}, err - } - - return adapterEvent{ - subscriberID: subscriberID, - topic: topic, - }, nil -} - -func toMqttSubscribeEvent(journal Journal) (adapterEvent, error) { - clientID, err := getStringAttribute(journal, "client_id") - if err != nil { - return adapterEvent{}, err - } - subscriberID, err := getStringAttribute(journal, "subscriber_id") - if err != nil { - return adapterEvent{}, err - } - channelID, err := getStringAttribute(journal, "channel_id") - if err != nil { - return adapterEvent{}, err - } - subtopic, err := getStringAttribute(journal, "subtopic") - if err != nil { - return adapterEvent{}, err - } - - return adapterEvent{ - clientID: clientID, - subscriberID: subscriberID, - channelID: channelID, - subtopic: subtopic, - }, nil -} - -func toMqttDisconnectEvent(journal Journal) (adapterEvent, error) { - subscriberID, err := getStringAttribute(journal, "subscriber_id") - if err != nil { - return adapterEvent{}, err - } - clientID, err := getStringAttribute(journal, "client_id") - if err != nil { - return adapterEvent{}, err - } - - return adapterEvent{ - subscriberID: subscriberID, - channelID: clientID, - }, nil -} - -func getStringAttribute(journal Journal, key string) (string, error) { - value, ok := journal.Attributes[key].(string) - if !ok { - return "", fmt.Errorf("missing or invalid %s attribute", key) - } - return value, nil -} diff --git a/journal/service_test.go b/journal/service_test.go deleted file mode 100644 index 8fe7836c0..000000000 --- a/journal/service_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package journal_test - -import ( - "context" - "fmt" - "math/rand" - "testing" - "time" - - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/journal" - "github.com/absmach/magistrala/journal/mocks" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - validJournal = journal.Journal{ - Operation: "user.create", - OccurredAt: time.Now().Add(-time.Hour), - Attributes: map[string]any{ - "temperature": rand.Float64(), - "humidity": rand.Float64(), - }, - Metadata: map[string]any{ - "sensor_id": rand.Intn(1000), - }, - } - idProvider = uuid.New() -) - -func TestSave(t *testing.T) { - repo := new(mocks.Repository) - svc := journal.NewService(idProvider, repo) - - cases := []struct { - desc string - journal journal.Journal - repoErr error - err error - }{ - { - desc: "successful with ID and EntityType", - journal: validJournal, - repoErr: nil, - err: nil, - }, - { - desc: "with repo error", - repoErr: repoerr.ErrCreateEntity, - err: repoerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("Save", context.Background(), mock.Anything).Return(tc.repoErr) - err := svc.Save(context.Background(), tc.journal) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - }) - } -} - -func TestReadAll(t *testing.T) { - repo := new(mocks.Repository) - svc := journal.NewService(idProvider, repo) - - validSession := smqauthn.Session{DomainUserID: testsutil.GenerateUUID(t), UserID: testsutil.GenerateUUID(t), DomainID: testsutil.GenerateUUID(t)} - validPage := journal.Page{ - Offset: 0, - Limit: 10, - EntityID: testsutil.GenerateUUID(t), - EntityType: journal.ClientEntity, - } - - cases := []struct { - desc string - session smqauthn.Session - page journal.Page - resp journal.JournalsPage - authErr error - repoErr error - err error - }{ - { - desc: "successful", - session: validSession, - page: validPage, - resp: journal.JournalsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Journals: []journal.Journal{validJournal}, - }, - authErr: nil, - repoErr: nil, - err: nil, - }, - { - desc: "successful for user", - session: validSession, - page: journal.Page{ - Offset: 0, - Limit: 10, - EntityID: testsutil.GenerateUUID(t), - EntityType: journal.UserEntity, - }, - resp: journal.JournalsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Journals: []journal.Journal{validJournal}, - }, - authErr: nil, - repoErr: nil, - err: nil, - }, - { - desc: "with repo error", - session: validSession, - page: validPage, - resp: journal.JournalsPage{}, - repoErr: repoerr.ErrViewEntity, - err: repoerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RetrieveAll", context.Background(), tc.page).Return(tc.resp, tc.repoErr) - resp, err := svc.RetrieveAll(context.Background(), tc.session, tc.page) - if tc.err == nil { - assert.Equal(t, tc.resp, resp, tc.desc) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - }) - } -} diff --git a/notifications/README.md b/notifications/README.md deleted file mode 100644 index 3ce05eb06..000000000 --- a/notifications/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Notifications Service - -The Notifications Service is responsible for sending email notifications when domain invitation events occur in the Magistrala platform. - -## Overview - -This service listens to invitation events from the domains service and sends email notifications to users when: -- They are invited to join a domain (`invitation.send`) -- Someone accepts their domain invitation (`invitation.accept`) -- Someone rejects their domain invitation (`invitation.reject`) - -The service fetches user information from Atom entities and sends styled email notifications using SMTP. - -## Features - -- **Event-Driven**: Listens to invitation events from the event store (NATS/RabbitMQ) -- **Atom Integration**: Fetches user details (name, email) from Atom -- **Beautiful Email Templates**: Styled HTML email templates with Magistrala branding (#083662) -- **Configurable**: Email server settings and templates are fully configurable - -## Architecture - -``` -domains service → event store → notifications service → Atom - ↓ - SMTP Server → Email Recipients -``` - -## Configuration - -The service is configured using environment variables: - -### General Configuration -- `MG_NOTIFICATIONS_LOG_LEVEL` - Log level (default: "info") -- `MG_NOTIFICATIONS_INSTANCE_ID` - Instance ID for the service -- `MG_NOTIFICATIONS_DOMAIN_ALT_NAME` - Alternative name for domains such as, say, workspaces or tenants (default: "domains") -- `MG_ES_URL` - Event store URL (default: "nats://localhost:4222") - -### Email Configuration -- `MG_EMAIL_HOST` - SMTP server host (default: "localhost") -- `MG_EMAIL_PORT` - SMTP server port (default: "25") -- `MG_EMAIL_USERNAME` - SMTP username -- `MG_EMAIL_PASSWORD` - SMTP password -- `MG_EMAIL_FROM_ADDRESS` - From email address (default: "noreply@magistrala.com") -- `MG_EMAIL_FROM_NAME` - From name (default: "Magistrala Notifications") - -### Template Configuration -- `MG_EMAIL_INVITATION_TEMPLATE` - Path to invitation email template -- `MG_EMAIL_ACCEPTANCE_TEMPLATE` - Path to acceptance email template -- `MG_EMAIL_REJECTION_TEMPLATE` - Path to rejection email template - -### Atom Configuration -- `ATOM_URL` - Atom HTTP URL -- `ATOM_SERVICE_TOKEN` - Service bearer token, if provisioned -- `ATOM_SERVICE_USERNAME` / `ATOM_SERVICE_SECRET` - Service credential fallback -- `ATOM_ADMIN_USERNAME` / `ATOM_ADMIN_SECRET` - Admin credential fallback -- `ATOM_TIMEOUT` - Atom HTTP timeout - -## Running the Service - -```bash -go run cmd/notifications/main.go -``` - -Or build and run: - -```bash -go build -o notifications cmd/notifications/main.go -./notifications -``` - -## Email Templates - -The service includes three beautifully styled email templates with Magistrala branding: - -1. **Invitation Sent** (`invitation-sent-email.tmpl`) - Blue gradient header (#083662) -2. **Invitation Accepted** (`invitation-accepted-email.tmpl`) - Green gradient header -3. **Invitation Rejected** (`invitation-rejected-email.tmpl`) - Red gradient header - -All templates are responsive and include: -- Professional styling -- Gradient headers -- Clear call-to-action sections -- Magistrala branding - -## Testing - -Run the tests: - -```bash -go test ./notifications/... -v -``` - -To run email integration tests (requires SMTP server): - -```bash -MG_RUN_EMAIL_TESTS=true go test ./notifications/emailer -v -``` - -## Development - -The service consists of: -- `notifier.go` - Main service interface -- `emailer/emailer.go` - Email notification implementation -- `events/consumer.go` - Event consumer for invitation events -- `cmd/notifications/main.go` - Service entry point -- Tests with mocks for unit testing - -## Dependencies - -- Atom - for fetching user information -- Event store (NATS/RabbitMQ) - for receiving invitation events -- SMTP server - for sending emails diff --git a/notifications/doc.go b/notifications/doc.go deleted file mode 100644 index 0fa62f6c1..000000000 --- a/notifications/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package notifications provides the domain model for the notifications service. -package notifications diff --git a/notifications/emailer/atom_users.go b/notifications/emailer/atom_users.go deleted file mode 100644 index bffc61a59..000000000 --- a/notifications/emailer/atom_users.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package emailer - -import ( - "context" - "fmt" - - "github.com/absmach/magistrala/internal/atom" -) - -// AtomUserResolver resolves notification users from Atom entities. -type AtomUserResolver struct { - client *atom.Client -} - -// NewAtomUserResolver creates an Atom-backed notification user resolver. -func NewAtomUserResolver(client *atom.Client) AtomUserResolver { - return AtomUserResolver{client: client} -} - -// FetchUsers loads users by ID from Atom. -func (r AtomUserResolver) FetchUsers(ctx context.Context, userIDs []string) (map[string]User, error) { - users := make(map[string]User, len(userIDs)) - for _, userID := range userIDs { - if userID == "" { - continue - } - entity, err := r.client.GetEntity(ctx, userID) - if err != nil { - return nil, fmt.Errorf("fetch atom entity %s: %w", userID, err) - } - users[userID] = atomEntityUser(entity) - } - return users, nil -} - -func atomEntityUser(entity atom.Entity) User { - email := attrString(entity.Attributes, "email") - if email == "" { - email = attrString(entity.Attributes, "primary_email") - } - username := attrString(entity.Attributes, "username") - if username == "" { - username = entity.Name - } - return User{ - ID: entity.ID, - Email: email, - Username: username, - FirstName: attrString(entity.Attributes, "first_name"), - LastName: attrString(entity.Attributes, "last_name"), - } -} - -func attrString(attrs atom.Attributes, key string) string { - value, ok := attrs[key] - if !ok || value == nil { - return "" - } - switch typed := value.(type) { - case string: - return typed - case fmt.Stringer: - return typed.String() - default: - return fmt.Sprint(typed) - } -} diff --git a/notifications/emailer/doc.go b/notifications/emailer/doc.go deleted file mode 100644 index 3a2744422..000000000 --- a/notifications/emailer/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package emailer provides email notification implementation for the notifications service. -package emailer diff --git a/notifications/emailer/emailer.go b/notifications/emailer/emailer.go deleted file mode 100644 index 56affef5c..000000000 --- a/notifications/emailer/emailer.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package emailer - -import ( - "context" - "fmt" - "unicode" - "unicode/utf8" - - "github.com/absmach/magistrala/internal/email" - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/pkg/errors" -) - -var ( - errFetchingUser = errors.New("failed to fetch user information") - errSendingEmail = errors.New("failed to send email") -) - -const ( - inviterRecipient = "inviter" - inviteeRecipient = "invitee" -) - -var _ notifications.Notifier = (*notifier)(nil) - -// User contains the user fields needed by notification templates. -type User struct { - ID string - Email string - Username string - FirstName string - LastName string -} - -// UserResolver resolves notification principals from the active identity store. -type UserResolver interface { - FetchUsers(ctx context.Context, userIDs []string) (map[string]User, error) -} - -type notifier struct { - users UserResolver - agents map[notifications.NotificationType]*email.Agent - fromName string - domainAltName string -} - -// Config represents the emailer configuration. -type Config struct { - FromAddress string - FromName string - DomainAltName string - InvitationTemplate string - AcceptanceTemplate string - RejectionTemplate string - EmailHost string - EmailPort string - EmailUsername string - EmailPassword string -} - -// New creates a new email notifier. -func New(users UserResolver, cfg Config) (notifications.Notifier, error) { - templates := map[notifications.NotificationType]string{ - notifications.Invitation: cfg.InvitationTemplate, - notifications.Acceptance: cfg.AcceptanceTemplate, - notifications.Rejection: cfg.RejectionTemplate, - } - - agents := make(map[notifications.NotificationType]*email.Agent) - for notifType, template := range templates { - emailCfg := &email.Config{ - Host: cfg.EmailHost, - Port: cfg.EmailPort, - Username: cfg.EmailUsername, - Password: cfg.EmailPassword, - FromAddress: cfg.FromAddress, - FromName: cfg.FromName, - Template: template, - } - agent, err := email.New(emailCfg) - if err != nil { - return nil, err - } - agents[notifType] = agent - } - - return ¬ifier{ - users: users, - agents: agents, - fromName: cfg.FromName, - domainAltName: cfg.DomainAltName, - }, nil -} - -func (n *notifier) Notify(ctx context.Context, notif notifications.Notification) error { - users, err := n.fetchUsers(ctx, []string{notif.InviterID, notif.InviteeID}) - if err != nil { - return errors.Wrap(errFetchingUser, err) - } - - inviter, ok := users[notif.InviterID] - if !ok { - return errors.Wrap(errFetchingUser, fmt.Errorf("inviter not found: %s", notif.InviterID)) - } - - invitee, ok := users[notif.InviteeID] - if !ok { - return errors.Wrap(errFetchingUser, fmt.Errorf("invitee not found: %s", notif.InviteeID)) - } - - inviterName := n.userDisplayName(inviter) - inviteeName := n.userDisplayName(invitee) - - domainName := notif.DomainName - if domainName == "" { - domainName = notif.DomainID - } - - roleName := notif.RoleName - if roleName == "" { - roleName = notif.RoleID - } - - subject, content, recipient, err := n.buildEmailContent(notif.Type, inviterName, inviteeName, domainName, roleName) - if err != nil { - return err - } - recipientEmail := inviter.Email - recipientName := inviterName - if recipient == inviteeRecipient { - recipientEmail = invitee.Email - recipientName = inviteeName - } - - agent, ok := n.agents[notif.Type] - if !ok || agent == nil { - return errors.Wrap(errSendingEmail, fmt.Errorf("no email agent configured for notification type: %d", notif.Type)) - } - - if err := agent.Send([]string{recipientEmail}, "", subject, "", recipientName, content, n.fromName, nil); err != nil { - return errors.Wrap(errSendingEmail, err) - } - - return nil -} - -func (n *notifier) buildEmailContent(notifType notifications.NotificationType, inviterName, inviteeName, domainName, roleName string) (subject, content, recipient string, err error) { - switch notifType { - case notifications.Invitation: - return fmt.Sprintf("%s Invitation", titleFirst(n.domainAltName)), - fmt.Sprintf("%s has invited you to join the %s %s as %s.", n.domainAltName, inviterName, domainName, roleName), - inviteeRecipient, - nil - case notifications.Acceptance: - return "Invitation Accepted", - fmt.Sprintf("%s has accepted your invitation to join the %s %s as %s.", n.domainAltName, inviteeName, domainName, roleName), - inviterRecipient, - nil - case notifications.Rejection: - return "Invitation Declined", - fmt.Sprintf("%s has declined your invitation to join the %s %s as %s.", n.domainAltName, inviteeName, domainName, roleName), - inviterRecipient, - nil - default: - return "", "", "", errors.Wrap(errSendingEmail, fmt.Errorf("unsupported notification type: %d", notifType)) - } -} - -func (n *notifier) fetchUsers(ctx context.Context, userIDs []string) (map[string]User, error) { - return n.users.FetchUsers(ctx, userIDs) -} - -func (n *notifier) userDisplayName(user User) string { - if user.FirstName != "" && user.LastName != "" { - return fmt.Sprintf("%s %s", user.FirstName, user.LastName) - } - if user.FirstName != "" { - return user.FirstName - } - if user.Username != "" { - return user.Username - } - if user.Email != "" { - return user.Email - } - return user.ID -} - -func titleFirst(s string) string { - if s == "" { - return s - } - r, size := utf8.DecodeRuneInString(s) - return string(unicode.ToUpper(r)) + s[size:] -} diff --git a/notifications/emailer/emailer_test.go b/notifications/emailer/emailer_test.go deleted file mode 100644 index 84833489e..000000000 --- a/notifications/emailer/emailer_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package emailer_test - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/notifications/emailer" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const ( - inviterID = "inviter-id" - inviteeID = "invitee-id" - domainID = "domain-id" - domainName = "Test Domain" - roleID = "role-id" - roleName = "Admin" - - inviterEmail = "inviter@example.com" - inviteeEmail = "invitee@example.com" - inviterFirst = "John" - inviterLast = "Doe" - inviteeFirst = "Jane" - inviteeLast = "Smith" - - envTrue = "true" -) - -type mockUsersClient struct { - mock.Mock -} - -func (m *mockUsersClient) FetchUsers(ctx context.Context, userIDs []string) (map[string]emailer.User, error) { - args := m.Called(ctx, userIDs) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]emailer.User), args.Error(1) -} - -func TestNotify(t *testing.T) { - if os.Getenv("MG_RUN_EMAIL_TESTS") != envTrue { - t.Skip("Skipping email tests. Set MG_RUN_EMAIL_TESTS=true to run.") - } - - usersClient := new(mockUsersClient) - - cfg := emailer.Config{ - FromAddress: "test@example.com", - FromName: "Test Service", - DomainAltName: "domain", - InvitationTemplate: "../../docker/templates/invitation-sent-email.tmpl", - AcceptanceTemplate: "../../docker/templates/invitation-accepted-email.tmpl", - RejectionTemplate: "../../docker/templates/invitation-rejected-email.tmpl", - EmailHost: "localhost", - EmailPort: "1025", - EmailUsername: "", - EmailPassword: "", - } - - notifier, err := emailer.New(usersClient, cfg) - assert.NoError(t, err) - assert.NotNil(t, notifier) - - cases := []struct { - desc string - notification notifications.Notification - setupMock func() - expectedError error - }{ - { - desc: "successful invitation notification", - notification: notifications.Notification{ - Type: notifications.Invitation, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - }, - setupMock: func() { - usersClient.On("FetchUsers", mock.Anything, mock.MatchedBy(func(userIDs []string) bool { - return len(userIDs) == 2 && - ((userIDs[0] == inviterID && userIDs[1] == inviteeID) || - (userIDs[0] == inviteeID && userIDs[1] == inviterID)) - })).Return(map[string]emailer.User{ - inviterID: { - ID: inviterID, - Email: inviterEmail, - FirstName: inviterFirst, - LastName: inviterLast, - }, - inviteeID: { - ID: inviteeID, - Email: inviteeEmail, - FirstName: inviteeFirst, - LastName: inviteeLast, - }, - }, nil).Once() - }, - expectedError: nil, - }, - { - desc: "successful acceptance notification", - notification: notifications.Notification{ - Type: notifications.Acceptance, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - }, - setupMock: func() { - usersClient.On("FetchUsers", mock.Anything, mock.MatchedBy(func(userIDs []string) bool { - return len(userIDs) == 2 - })).Return(map[string]emailer.User{ - inviterID: { - ID: inviterID, - Email: inviterEmail, - FirstName: inviterFirst, - LastName: inviterLast, - }, - inviteeID: { - ID: inviteeID, - Email: inviteeEmail, - FirstName: inviteeFirst, - LastName: inviteeLast, - }, - }, nil).Once() - }, - expectedError: nil, - }, - { - desc: "successful rejection notification", - notification: notifications.Notification{ - Type: notifications.Rejection, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - }, - setupMock: func() { - usersClient.On("FetchUsers", mock.Anything, mock.MatchedBy(func(userIDs []string) bool { - return len(userIDs) == 2 - })).Return(map[string]emailer.User{ - inviterID: { - ID: inviterID, - Email: inviterEmail, - FirstName: inviterFirst, - LastName: inviterLast, - }, - inviteeID: { - ID: inviteeID, - Email: inviteeEmail, - FirstName: inviteeFirst, - LastName: inviteeLast, - }, - }, nil).Once() - }, - expectedError: nil, - }, - { - desc: "failed to fetch users", - notification: notifications.Notification{ - Type: notifications.Invitation, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - }, - setupMock: func() { - usersClient.On("FetchUsers", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("atom error")).Once() - }, - expectedError: fmt.Errorf("atom error"), - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - tc.setupMock() - err := notifier.Notify(context.Background(), tc.notification) - if tc.expectedError != nil { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - usersClient.AssertExpectations(t) - }) - } -} diff --git a/notifications/events/consumer.go b/notifications/events/consumer.go deleted file mode 100644 index abe9b74bf..000000000 --- a/notifications/events/consumer.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "context" - "fmt" - "log/slog" - - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/pkg/events" -) - -const ( - // Stream names. - sendInvitationStream = "events.magistrala.invitation.send" - acceptInvitationStream = "events.magistrala.invitation.accept" - rejectInvitationStream = "events.magistrala.invitation.reject" - - // Event data field keys. - invitedByKey = "invited_by" - inviteeUserIDKey = "invitee_user_id" - domainIDKey = "domain_id" - domainNameKey = "domain_name" - roleIDKey = "role_id" - roleNameKey = "role_name" -) - -// Start starts consuming invitation events from the event store. -func Start(ctx context.Context, consumer string, sub events.Subscriber, notifier notifications.Notifier) error { - handlers := []struct { - stream string - notifType notifications.NotificationType - errorCtx string - }{ - {sendInvitationStream, notifications.Invitation, "invitation sent"}, - {acceptInvitationStream, notifications.Acceptance, "invitation accepted"}, - {rejectInvitationStream, notifications.Rejection, "invitation rejected"}, - } - - for _, h := range handlers { - config := events.SubscriberConfig{ - Consumer: consumer, - Stream: h.stream, - Handler: handleInvitationEvent(notifier, h.notifType, h.errorCtx), - } - if err := sub.Subscribe(ctx, config); err != nil { - return err - } - } - - return nil -} - -func handleInvitationEvent(notifier notifications.Notifier, notifType notifications.NotificationType, errorContext string) handleFunc { - return func(ctx context.Context, event events.Event) error { - n, err := parseNotificationFromEvent(event, errorContext) - if err != nil { - return nil - } - - n.Type = notifType - - if err := notifier.Notify(ctx, n); err != nil { - slog.Error("failed to send notification", "error", err, "type", notifType, "context", errorContext) - } - - return nil - } -} - -func parseNotificationFromEvent(event events.Event, errorContext string) (notifications.Notification, error) { - data, err := event.Encode() - if err != nil { - slog.Error(fmt.Sprintf("failed to encode %s event", errorContext), "error", err) - return notifications.Notification{}, err - } - - invitedBy, ok := data[invitedByKey].(string) - if !ok || invitedBy == "" { - slog.Error(fmt.Sprintf("missing or invalid %s in %s event", invitedByKey, errorContext)) - return notifications.Notification{}, fmt.Errorf("missing or invalid %s", invitedByKey) - } - - inviteeUserID, ok := data[inviteeUserIDKey].(string) - if !ok || inviteeUserID == "" { - slog.Error(fmt.Sprintf("missing or invalid %s in %s event", inviteeUserIDKey, errorContext)) - return notifications.Notification{}, fmt.Errorf("missing or invalid %s", inviteeUserIDKey) - } - - domainID, ok := data[domainIDKey].(string) - if !ok || domainID == "" { - slog.Error(fmt.Sprintf("missing or invalid %s in %s event", domainIDKey, errorContext)) - return notifications.Notification{}, fmt.Errorf("missing or invalid %s", domainIDKey) - } - - // Optional fields - log if present but wrong type - roleID := optionalString(data, roleIDKey, errorContext) - domainName := optionalString(data, domainNameKey, errorContext) - roleName := optionalString(data, roleNameKey, errorContext) - - return notifications.Notification{ - InviterID: invitedBy, - InviteeID: inviteeUserID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - }, nil -} - -func optionalString(data map[string]any, key, errorContext string) string { - val, exists := data[key] - if !exists { - return "" - } - strVal, ok := val.(string) - if !ok { - slog.Warn(fmt.Sprintf("field %s in %s event has wrong type, expected string", key, errorContext), "actual_type", fmt.Sprintf("%T", val)) - return "" - } - return strVal -} - -type handleFunc func(ctx context.Context, event events.Event) error - -func (h handleFunc) Handle(ctx context.Context, event events.Event) error { - return h(ctx, event) -} - -func (h handleFunc) Cancel() error { - return nil -} diff --git a/notifications/events/consumer_test.go b/notifications/events/consumer_test.go deleted file mode 100644 index b2d6b385a..000000000 --- a/notifications/events/consumer_test.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events_test - -import ( - "context" - "testing" - - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/notifications/events" - "github.com/absmach/magistrala/notifications/mocks" - smqevents "github.com/absmach/magistrala/pkg/events" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const ( - inviterID = "inviter-id" - inviteeID = "invitee-id" - domainID = "domain-id" - domainName = "Test Domain" - roleID = "role-id" - roleName = "Admin" -) - -type testEvent struct { - data map[string]any -} - -func (e testEvent) Encode() (map[string]any, error) { - return e.data, nil -} - -type mockSubscriber struct { - mock.Mock -} - -func (m *mockSubscriber) Subscribe(ctx context.Context, cfg smqevents.SubscriberConfig) error { - args := m.Called(ctx, cfg) - return args.Error(0) -} - -func (m *mockSubscriber) Close() error { - args := m.Called() - return args.Error(0) -} - -func TestStart(t *testing.T) { - notifier := new(mocks.Notifier) - subscriber := new(mockSubscriber) - - subscriber.On("Subscribe", mock.Anything, mock.Anything).Return(nil).Times(3) - - err := events.Start(context.Background(), "test-consumer", subscriber, notifier) - assert.NoError(t, err) - subscriber.AssertExpectations(t) -} - -func TestHandleInvitationSent(t *testing.T) { - cases := []struct { - desc string - event smqevents.Event - mockCall bool - }{ - { - desc: "successful invitation sent handling", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "invitee_user_id": inviteeID, - "domain_id": domainID, - "domain_name": domainName, - "role_id": roleID, - "role_name": roleName, - }, - }, - mockCall: true, - }, - { - desc: "missing invited_by", - event: testEvent{ - data: map[string]any{ - "invitee_user_id": inviteeID, - "domain_id": domainID, - }, - }, - mockCall: false, - }, - { - desc: "missing invitee_user_id", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "domain_id": domainID, - }, - }, - mockCall: false, - }, - { - desc: "missing domain_id", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "invitee_user_id": inviteeID, - }, - }, - mockCall: false, - }, - { - desc: "optional fields with wrong type", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "invitee_user_id": inviteeID, - "domain_id": domainID, - "domain_name": domainName, - "role_id": 123, // wrong type: int instead of string - "role_name": true, // wrong type: bool instead of string - }, - }, - mockCall: true, // Should still process with empty role_id and role_name - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - notifier := new(mocks.Notifier) - subscriber := new(mockSubscriber) - - var handlerConfig smqevents.SubscriberConfig - subscriber.On("Subscribe", mock.Anything, mock.MatchedBy(func(cfg smqevents.SubscriberConfig) bool { - if cfg.Stream == "events.magistrala.invitation.send" { - handlerConfig = cfg - return true - } - return false - })).Return(nil).Once() - subscriber.On("Subscribe", mock.Anything, mock.Anything).Return(nil).Times(2) - - err := events.Start(context.Background(), "test-consumer", subscriber, notifier) - assert.NoError(t, err) - - if tc.mockCall { - expectedNotif := notifications.Notification{ - Type: notifications.Invitation, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - } - // For the "wrong type" test case, expect empty role fields - if tc.desc == "optional fields with wrong type" { - expectedNotif.RoleID = "" - expectedNotif.RoleName = "" - } - notifier.On("Notify", mock.Anything, expectedNotif).Return(nil).Once() - } - - err = handlerConfig.Handler.Handle(context.Background(), tc.event) - assert.NoError(t, err) - - if tc.mockCall { - notifier.AssertExpectations(t) - } - }) - } -} - -func TestHandleInvitationAccepted(t *testing.T) { - cases := []struct { - desc string - event smqevents.Event - mockCall bool - }{ - { - desc: "successful invitation accepted handling", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "invitee_user_id": inviteeID, - "domain_id": domainID, - "domain_name": domainName, - "role_id": roleID, - "role_name": roleName, - }, - }, - mockCall: true, - }, - { - desc: "missing invited_by", - event: testEvent{ - data: map[string]any{ - "invitee_user_id": inviteeID, - "domain_id": domainID, - }, - }, - mockCall: false, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - notifier := new(mocks.Notifier) - subscriber := new(mockSubscriber) - - var handlerConfig smqevents.SubscriberConfig - subscriber.On("Subscribe", mock.Anything, mock.MatchedBy(func(cfg smqevents.SubscriberConfig) bool { - if cfg.Stream == "events.magistrala.invitation.accept" { - handlerConfig = cfg - return true - } - return false - })).Return(nil).Once() - subscriber.On("Subscribe", mock.Anything, mock.Anything).Return(nil).Times(2) - - err := events.Start(context.Background(), "test-consumer", subscriber, notifier) - assert.NoError(t, err) - - if tc.mockCall { - expectedNotif := notifications.Notification{ - Type: notifications.Acceptance, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - } - notifier.On("Notify", mock.Anything, expectedNotif).Return(nil).Once() - } - - err = handlerConfig.Handler.Handle(context.Background(), tc.event) - assert.NoError(t, err) - - if tc.mockCall { - notifier.AssertExpectations(t) - } - }) - } -} - -func TestHandleInvitationRejected(t *testing.T) { - cases := []struct { - desc string - event smqevents.Event - mockCall bool - }{ - { - desc: "successful invitation rejected handling", - event: testEvent{ - data: map[string]any{ - "invited_by": inviterID, - "invitee_user_id": inviteeID, - "domain_id": domainID, - "domain_name": domainName, - "role_id": roleID, - "role_name": roleName, - }, - }, - mockCall: true, - }, - { - desc: "missing invited_by", - event: testEvent{ - data: map[string]any{ - "invitee_user_id": inviteeID, - "domain_id": domainID, - }, - }, - mockCall: false, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - notifier := new(mocks.Notifier) - subscriber := new(mockSubscriber) - - var handlerConfig smqevents.SubscriberConfig - subscriber.On("Subscribe", mock.Anything, mock.MatchedBy(func(cfg smqevents.SubscriberConfig) bool { - if cfg.Stream == "events.magistrala.invitation.reject" { - handlerConfig = cfg - return true - } - return false - })).Return(nil).Once() - subscriber.On("Subscribe", mock.Anything, mock.Anything).Return(nil).Times(2) - - err := events.Start(context.Background(), "test-consumer", subscriber, notifier) - assert.NoError(t, err) - - if tc.mockCall { - expectedNotif := notifications.Notification{ - Type: notifications.Rejection, - InviterID: inviterID, - InviteeID: inviteeID, - DomainID: domainID, - DomainName: domainName, - RoleID: roleID, - RoleName: roleName, - } - notifier.On("Notify", mock.Anything, expectedNotif).Return(nil).Once() - } - - err = handlerConfig.Handler.Handle(context.Background(), tc.event) - assert.NoError(t, err) - - if tc.mockCall { - notifier.AssertExpectations(t) - } - }) - } -} diff --git a/notifications/events/doc.go b/notifications/events/doc.go deleted file mode 100644 index 86685ce63..000000000 --- a/notifications/events/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package events provides event handling for the notifications service. -package events diff --git a/notifications/middleware/doc.go b/notifications/middleware/doc.go deleted file mode 100644 index 1e59754ac..000000000 --- a/notifications/middleware/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package middleware provides middleware for the notifications service. -package middleware diff --git a/notifications/middleware/logging.go b/notifications/middleware/logging.go deleted file mode 100644 index 1e16d2789..000000000 --- a/notifications/middleware/logging.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/notifications" -) - -var _ notifications.Notifier = (*loggingMiddleware)(nil) - -type loggingMiddleware struct { - logger *slog.Logger - notifier notifications.Notifier -} - -// NewLogging adds logging facilities to the notifier. -func NewLogging(notifier notifications.Notifier, logger *slog.Logger) notifications.Notifier { - return &loggingMiddleware{ - logger: logger, - notifier: notifier, - } -} - -func (lm *loggingMiddleware) Notify(ctx context.Context, n notifications.Notification) (err error) { - defer func(begin time.Time) { - groupName := notificationTypeToString(n.Type) - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.Group(groupName, - slog.String("inviter_id", n.InviterID), - slog.String("invitee_id", n.InviteeID), - slog.String("domain_id", n.DomainID), - slog.String("domain_name", n.DomainName), - slog.String("role_id", n.RoleID), - slog.String("role_name", n.RoleName), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Send "+groupName+" notification failed", args...) - return - } - lm.logger.Info("Send "+groupName+" notification completed successfully", args...) - }(time.Now()) - - return lm.notifier.Notify(ctx, n) -} - -func notificationTypeToString(t notifications.NotificationType) string { - switch t { - case notifications.Invitation: - return "invitation" - case notifications.Acceptance: - return "acceptance" - case notifications.Rejection: - return "rejection" - default: - return "unknown" - } -} diff --git a/notifications/middleware/logging_test.go b/notifications/middleware/logging_test.go deleted file mode 100644 index 25efbbe26..000000000 --- a/notifications/middleware/logging_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware_test - -import ( - "context" - "log/slog" - "os" - "testing" - - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/notifications/middleware" - "github.com/absmach/magistrala/notifications/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestLoggingMiddleware(t *testing.T) { - notifier := new(mocks.Notifier) - logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - lm := middleware.NewLogging(notifier, logger) - - cases := []struct { - desc string - notification notifications.Notification - expectedErr error - }{ - { - desc: "send invitation notification successfully", - notification: notifications.Notification{ - Type: notifications.Invitation, - InviterID: "inviter-1", - InviteeID: "invitee-1", - DomainID: "domain-1", - DomainName: "Test Domain", - RoleID: "role-1", - RoleName: "Admin", - }, - expectedErr: nil, - }, - { - desc: "send acceptance notification successfully", - notification: notifications.Notification{ - Type: notifications.Acceptance, - InviterID: "inviter-1", - InviteeID: "invitee-1", - DomainID: "domain-1", - DomainName: "Test Domain", - RoleID: "role-1", - RoleName: "Admin", - }, - expectedErr: nil, - }, - { - desc: "send rejection notification successfully", - notification: notifications.Notification{ - Type: notifications.Rejection, - InviterID: "inviter-1", - InviteeID: "invitee-1", - DomainID: "domain-1", - DomainName: "Test Domain", - RoleID: "role-1", - RoleName: "Admin", - }, - expectedErr: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - notifier.On("Notify", mock.Anything, tc.notification).Return(tc.expectedErr).Once() - err := lm.Notify(context.Background(), tc.notification) - assert.Equal(t, tc.expectedErr, err) - notifier.AssertExpectations(t) - }) - } -} diff --git a/notifications/middleware/metrics.go b/notifications/middleware/metrics.go deleted file mode 100644 index 4622f9d02..000000000 --- a/notifications/middleware/metrics.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/notifications" - "github.com/go-kit/kit/metrics" -) - -var _ notifications.Notifier = (*metricsMiddleware)(nil) - -type metricsMiddleware struct { - counter metrics.Counter - latency metrics.Histogram - notifier notifications.Notifier -} - -// NewMetrics returns new notifier with metrics middleware. -func NewMetrics(notifier notifications.Notifier, counter metrics.Counter, latency metrics.Histogram) notifications.Notifier { - return &metricsMiddleware{ - counter: counter, - latency: latency, - notifier: notifier, - } -} - -func (mm *metricsMiddleware) Notify(ctx context.Context, n notifications.Notification) error { - defer func(begin time.Time) { - methodName := notificationTypeToMethodName(n.Type) - mm.counter.With("method", methodName).Add(1) - mm.latency.With("method", methodName).Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.notifier.Notify(ctx, n) -} diff --git a/notifications/middleware/metrics_test.go b/notifications/middleware/metrics_test.go deleted file mode 100644 index 7b2dcaba3..000000000 --- a/notifications/middleware/metrics_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware_test - -import ( - "context" - "testing" - - "github.com/absmach/magistrala/notifications" - "github.com/absmach/magistrala/notifications/middleware" - "github.com/absmach/magistrala/notifications/mocks" - "github.com/go-kit/kit/metrics" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -type mockCounter struct { - mock.Mock - metrics.Counter -} - -func (m *mockCounter) Add(delta float64) { - m.Called(delta) -} - -func (m *mockCounter) With(labelValues ...string) metrics.Counter { - args := m.Called(labelValues) - return args.Get(0).(metrics.Counter) -} - -type mockHistogram struct { - mock.Mock - metrics.Histogram -} - -func (m *mockHistogram) Observe(value float64) { - m.Called(value) -} - -func (m *mockHistogram) With(labelValues ...string) metrics.Histogram { - args := m.Called(labelValues) - return args.Get(0).(metrics.Histogram) -} - -func TestMetricsMiddleware(t *testing.T) { - notifier := new(mocks.Notifier) - counter := new(mockCounter) - histogram := new(mockHistogram) - - counter.On("With", mock.Anything).Return(counter) - counter.On("Add", mock.Anything).Return() - histogram.On("With", mock.Anything).Return(histogram) - histogram.On("Observe", mock.Anything).Return() - - mm := middleware.NewMetrics(notifier, counter, histogram) - - notif := notifications.Notification{ - Type: notifications.Invitation, - InviterID: "inv1", - InviteeID: "inv2", - DomainID: "dom1", - DomainName: "Domain", - RoleID: "role1", - RoleName: "Admin", - } - - notifier.On("Notify", mock.Anything, notif).Return(nil).Once() - - err := mm.Notify(context.Background(), notif) - assert.NoError(t, err) - notifier.AssertExpectations(t) - counter.AssertCalled(t, "With", []string{"method", "send_invitation_notification"}) - counter.AssertCalled(t, "Add", mock.Anything) - histogram.AssertCalled(t, "With", []string{"method", "send_invitation_notification"}) - histogram.AssertCalled(t, "Observe", mock.Anything) -} diff --git a/notifications/middleware/tracing.go b/notifications/middleware/tracing.go deleted file mode 100644 index c596a4520..000000000 --- a/notifications/middleware/tracing.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/notifications" - smqTracing "github.com/absmach/magistrala/pkg/tracing" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -var _ notifications.Notifier = (*tracing)(nil) - -type tracing struct { - tracer trace.Tracer - notifier notifications.Notifier -} - -// NewTracing returns a new notifier with tracing capabilities. -func NewTracing(notifier notifications.Notifier, tracer trace.Tracer) notifications.Notifier { - return &tracing{tracer, notifier} -} - -func (tm *tracing) Notify(ctx context.Context, n notifications.Notification) error { - spanName := notificationTypeToMethodName(n.Type) - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, spanName, trace.WithAttributes( - attribute.String("inviter_id", n.InviterID), - attribute.String("invitee_id", n.InviteeID), - attribute.String("domain_id", n.DomainID), - attribute.String("domain_name", n.DomainName), - attribute.String("role_id", n.RoleID), - attribute.String("role_name", n.RoleName), - )) - defer span.End() - - return tm.notifier.Notify(ctx, n) -} - -func notificationTypeToMethodName(t notifications.NotificationType) string { - switch t { - case notifications.Invitation: - return "send_invitation_notification" - case notifications.Acceptance: - return "send_acceptance_notification" - case notifications.Rejection: - return "send_rejection_notification" - default: - return "unknown" - } -} diff --git a/notifications/mocks/doc.go b/notifications/mocks/doc.go deleted file mode 100644 index cd464bba1..000000000 --- a/notifications/mocks/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package mocks provides mocks for the notifications service. -package mocks diff --git a/notifications/mocks/notifier.go b/notifications/mocks/notifier.go deleted file mode 100644 index b8c5491a9..000000000 --- a/notifications/mocks/notifier.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/notifications" - mock "github.com/stretchr/testify/mock" -) - -// NewNotifier creates a new instance of Notifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewNotifier(t interface { - mock.TestingT - Cleanup(func()) -}) *Notifier { - mock := &Notifier{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Notifier is an autogenerated mock type for the Notifier type -type Notifier struct { - mock.Mock -} - -type Notifier_Expecter struct { - mock *mock.Mock -} - -func (_m *Notifier) EXPECT() *Notifier_Expecter { - return &Notifier_Expecter{mock: &_m.Mock} -} - -// Notify provides a mock function for the type Notifier -func (_mock *Notifier) Notify(ctx context.Context, n notifications.Notification) error { - ret := _mock.Called(ctx, n) - - if len(ret) == 0 { - panic("no return value specified for Notify") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, notifications.Notification) error); ok { - r0 = returnFunc(ctx, n) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Notifier_Notify_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Notify' -type Notifier_Notify_Call struct { - *mock.Call -} - -// Notify is a helper method to define mock.On call -// - ctx context.Context -// - n notifications.Notification -func (_e *Notifier_Expecter) Notify(ctx interface{}, n interface{}) *Notifier_Notify_Call { - return &Notifier_Notify_Call{Call: _e.mock.On("Notify", ctx, n)} -} - -func (_c *Notifier_Notify_Call) Run(run func(ctx context.Context, n notifications.Notification)) *Notifier_Notify_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 notifications.Notification - if args[1] != nil { - arg1 = args[1].(notifications.Notification) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Notifier_Notify_Call) Return(err error) *Notifier_Notify_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Notifier_Notify_Call) RunAndReturn(run func(ctx context.Context, n notifications.Notification) error) *Notifier_Notify_Call { - _c.Call.Return(run) - return _c -} diff --git a/notifications/notifier.go b/notifications/notifier.go deleted file mode 100644 index 349e36256..000000000 --- a/notifications/notifier.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package notifications - -import ( - "context" -) - -// NotificationType represents the type of notification to send. -type NotificationType uint8 - -const ( - // Invitation represents an invitation notification. - Invitation NotificationType = iota - // Acceptance represents an acceptance notification. - Acceptance - // Rejection represents a rejection notification. - Rejection -) - -// Notification contains the data needed to send a notification. -type Notification struct { - Type NotificationType - InviterID string - InviteeID string - DomainID string - DomainName string - RoleID string - RoleName string -} - -// Notifier represents a service for sending notifications. -type Notifier interface { - // Notify sends a notification based on the provided notification data. - Notify(ctx context.Context, n Notification) error -} diff --git a/internal/atom/authz.go b/pkg/atom/authz.go similarity index 100% rename from internal/atom/authz.go rename to pkg/atom/authz.go diff --git a/internal/atom/authz_compat.go b/pkg/atom/authz_compat.go similarity index 100% rename from internal/atom/authz_compat.go rename to pkg/atom/authz_compat.go diff --git a/internal/atom/authz_test.go b/pkg/atom/authz_test.go similarity index 98% rename from internal/atom/authz_test.go rename to pkg/atom/authz_test.go index 718e601a3..2e7b1244e 100644 --- a/internal/atom/authz_test.go +++ b/pkg/atom/authz_test.go @@ -8,7 +8,7 @@ import ( "testing" channelsv1 "github.com/absmach/magistrala/api/grpc/channels/v1" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/connections" "github.com/absmach/magistrala/pkg/errors" diff --git a/internal/atom/bootstrap.go b/pkg/atom/bootstrap.go similarity index 100% rename from internal/atom/bootstrap.go rename to pkg/atom/bootstrap.go diff --git a/internal/atom/bootstrap_test.go b/pkg/atom/bootstrap_test.go similarity index 100% rename from internal/atom/bootstrap_test.go rename to pkg/atom/bootstrap_test.go diff --git a/internal/atom/client.go b/pkg/atom/client.go similarity index 100% rename from internal/atom/client.go rename to pkg/atom/client.go diff --git a/internal/atom/client_test.go b/pkg/atom/client_test.go similarity index 100% rename from internal/atom/client_test.go rename to pkg/atom/client_test.go diff --git a/internal/atom/config.go b/pkg/atom/config.go similarity index 100% rename from internal/atom/config.go rename to pkg/atom/config.go diff --git a/internal/atom/constants.go b/pkg/atom/constants.go similarity index 98% rename from internal/atom/constants.go rename to pkg/atom/constants.go index 485e66832..cb227b459 100644 --- a/internal/atom/constants.go +++ b/pkg/atom/constants.go @@ -83,8 +83,6 @@ const ( const atomAttributeSourceMagistrala = "magistrala" -const atomServiceTokenJournal = "journal" - const ( atomGraphQLPath = "/graphql" atomAuthIntrospectPath = "/auth/introspect" diff --git a/pkg/atom/doc.go b/pkg/atom/doc.go new file mode 100644 index 000000000..d93e5c037 --- /dev/null +++ b/pkg/atom/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package atom provides the shared Atom authentication, authorization, +// projection, bootstrap, and service-token integration used by Magistrala +// Community and Enterprise services. +package atom diff --git a/internal/atom/grpc_compat.go b/pkg/atom/grpc_compat.go similarity index 100% rename from internal/atom/grpc_compat.go rename to pkg/atom/grpc_compat.go diff --git a/internal/atom/grpc_compat_test.go b/pkg/atom/grpc_compat_test.go similarity index 100% rename from internal/atom/grpc_compat_test.go rename to pkg/atom/grpc_compat_test.go diff --git a/internal/atom/mapping.go b/pkg/atom/mapping.go similarity index 100% rename from internal/atom/mapping.go rename to pkg/atom/mapping.go diff --git a/internal/atom/mapping_test.go b/pkg/atom/mapping_test.go similarity index 100% rename from internal/atom/mapping_test.go rename to pkg/atom/mapping_test.go diff --git a/internal/atom/policy.go b/pkg/atom/policy.go similarity index 100% rename from internal/atom/policy.go rename to pkg/atom/policy.go diff --git a/internal/atom/policy_service.go b/pkg/atom/policy_service.go similarity index 100% rename from internal/atom/policy_service.go rename to pkg/atom/policy_service.go diff --git a/internal/atom/policy_service_test.go b/pkg/atom/policy_service_test.go similarity index 100% rename from internal/atom/policy_service_test.go rename to pkg/atom/policy_service_test.go diff --git a/internal/atom/policy_test.go b/pkg/atom/policy_test.go similarity index 96% rename from internal/atom/policy_test.go rename to pkg/atom/policy_test.go index f47f2614d..173410057 100644 --- a/internal/atom/policy_test.go +++ b/pkg/atom/policy_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/absmach/magistrala/internal/atom" + "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/errors" "github.com/absmach/magistrala/pkg/policies" "github.com/stretchr/testify/assert" diff --git a/internal/atom/projector.go b/pkg/atom/projector.go similarity index 100% rename from internal/atom/projector.go rename to pkg/atom/projector.go diff --git a/internal/atom/service_tokens.go b/pkg/atom/service_tokens.go similarity index 96% rename from internal/atom/service_tokens.go rename to pkg/atom/service_tokens.go index 89b42061e..43401c011 100644 --- a/internal/atom/service_tokens.go +++ b/pkg/atom/service_tokens.go @@ -40,8 +40,6 @@ func DefaultServiceTokenSpecs() []ServiceTokenSpec { {Name: "fluxmq-node1", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE1", Description: "Magistrala Docker Compose token for fluxmq-node1"}, {Name: "fluxmq-node2", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE2", Description: "Magistrala Docker Compose token for fluxmq-node2"}, {Name: "fluxmq-node3", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE3", Description: "Magistrala Docker Compose token for fluxmq-node3"}, - {Name: atomServiceTokenJournal, Env: "MG_ATOM_TOKEN_JOURNAL", Description: "Magistrala Docker Compose token for journal"}, - {Name: "notifications", Env: "MG_ATOM_TOKEN_NOTIFICATIONS", Description: "Magistrala Docker Compose token for notifications"}, {Name: "timescale-reader", Env: "MG_ATOM_TOKEN_TIMESCALE_READER", Description: "Magistrala Docker Compose token for timescale-reader"}, {Name: "re", Env: "MG_ATOM_TOKEN_RE", Description: "Magistrala Docker Compose token for rule engine"}, {Name: "alarms", Env: "MG_ATOM_TOKEN_ALARMS", Description: "Magistrala Docker Compose token for alarms"}, diff --git a/internal/atom/service_tokens_test.go b/pkg/atom/service_tokens_test.go similarity index 98% rename from internal/atom/service_tokens_test.go rename to pkg/atom/service_tokens_test.go index aaa12560a..8548ff27d 100644 --- a/internal/atom/service_tokens_test.go +++ b/pkg/atom/service_tokens_test.go @@ -96,7 +96,7 @@ func TestProvisionServiceTokensRotatesToken(t *testing.T) { result, err := ProvisionServiceTokens(context.Background(), fake.Client(), TokenProvisionOptions{ OutputPath: output, - Rotate: "journal", + Rotate: "test-service", Specs: []ServiceTokenSpec{testTokenSpec()}, }) if err != nil { @@ -233,7 +233,7 @@ func (f *fakeAtomTokenServer) handleGraphQL(w http.ResponseWriter, r *http.Reque } func testTokenSpec() ServiceTokenSpec { - return ServiceTokenSpec{Name: "journal", Env: "MG_ATOM_TOKEN_JOURNAL", Description: "test journal token"} + return ServiceTokenSpec{Name: "test-service", Env: "MG_ATOM_TOKEN_TEST_SERVICE", Description: "test service token"} } func accessTokenForCredentialID(id string) string { diff --git a/internal/atom/test_constants_test.go b/pkg/atom/test_constants_test.go similarity index 100% rename from internal/atom/test_constants_test.go rename to pkg/atom/test_constants_test.go diff --git a/internal/atom/token.go b/pkg/atom/token.go similarity index 100% rename from internal/atom/token.go rename to pkg/atom/token.go diff --git a/internal/atom/token_test.go b/pkg/atom/token_test.go similarity index 100% rename from internal/atom/token_test.go rename to pkg/atom/token_test.go diff --git a/internal/atom/types.go b/pkg/atom/types.go similarity index 100% rename from internal/atom/types.go rename to pkg/atom/types.go diff --git a/pkg/authn/atom/authn.go b/pkg/authn/atom/authn.go index 14822a95c..5af985f6e 100644 --- a/pkg/authn/atom/authn.go +++ b/pkg/authn/atom/authn.go @@ -6,7 +6,7 @@ package atom import ( "context" - atomcore "github.com/absmach/magistrala/internal/atom" + atomcore "github.com/absmach/magistrala/pkg/atom" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" diff --git a/internal/email/README.md b/pkg/email/README.md similarity index 100% rename from internal/email/README.md rename to pkg/email/README.md diff --git a/internal/email/doc.go b/pkg/email/doc.go similarity index 100% rename from internal/email/doc.go rename to pkg/email/doc.go diff --git a/internal/email/email.go b/pkg/email/email.go similarity index 100% rename from internal/email/email.go rename to pkg/email/email.go diff --git a/pkg/emailer/emailer.go b/pkg/emailer/emailer.go index bd85dd694..a339a5540 100644 --- a/pkg/emailer/emailer.go +++ b/pkg/emailer/emailer.go @@ -4,7 +4,7 @@ package emailer import ( - "github.com/absmach/magistrala/internal/email" + "github.com/absmach/magistrala/pkg/email" ) var _ Emailer = (*emailer)(nil) diff --git a/consumers/writers/brokers/brokers_fluxmq.go b/pkg/messaging/writers/brokers_fluxmq.go similarity index 98% rename from consumers/writers/brokers/brokers_fluxmq.go rename to pkg/messaging/writers/brokers_fluxmq.go index d31c1e179..728b9cabe 100644 --- a/consumers/writers/brokers/brokers_fluxmq.go +++ b/pkg/messaging/writers/brokers_fluxmq.go @@ -4,7 +4,7 @@ //go:build msg_fluxmq // +build msg_fluxmq -package brokers +package writers import ( "context" diff --git a/consumers/writers/brokers/brokers_nats.go b/pkg/messaging/writers/brokers_nats.go similarity index 98% rename from consumers/writers/brokers/brokers_nats.go rename to pkg/messaging/writers/brokers_nats.go index c480f7533..50a1226be 100644 --- a/consumers/writers/brokers/brokers_nats.go +++ b/pkg/messaging/writers/brokers_nats.go @@ -4,7 +4,7 @@ //go:build !msg_fluxmq && !msg_rabbitmq && !rabbitmq // +build !msg_fluxmq,!msg_rabbitmq,!rabbitmq -package brokers +package writers import ( "context" diff --git a/pkg/re/events/consumer/decode.go b/pkg/re/events/consumer/decode.go deleted file mode 100644 index 41066aa77..000000000 --- a/pkg/re/events/consumer/decode.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package consumer - -import ( - "encoding/json" - "time" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/re" -) - -var ( - errDecodeAddRuleEvent = errors.New("failed to decode rule add event") - errDecodeUpdateRuleEvent = errors.New("failed to decode rule update event") - errDecodeUpdateRuleTagsEvent = errors.New("failed to decode rule update tags event") - errDecodeUpdateRuleScheduleEvent = errors.New("failed to decode rule update schedule event") - errDecodeEnableRuleEvent = errors.New("failed to decode rule enable event") - errDecodeDisableRuleEvent = errors.New("failed to decode rule disable event") - errDecodeRemoveRuleEvent = errors.New("failed to decode rule remove event") - - errID = errors.New("missing or invalid 'id'") - errName = errors.New("missing or invalid 'name'") - errTags = errors.New("invalid 'tags'") - errStatus = errors.New("missing or invalid 'status'") - errConvertStatus = errors.New("failed to convert status") - errCreatedBy = errors.New("missing or invalid 'created_by'") - errCreatedAt = errors.New("failed to parse 'created_at' time") - errUpdatedAt = errors.New("failed to parse 'updated_at' time") - errDecodeLogic = errors.New("failed to decode 'logic'") - errDecodeSchedule = errors.New("failed to decode 'schedule'") - errStringValue = errors.New("invalid string value") -) - -// ToRule decodes a map[string]any event payload into a re.Rule. -func ToRule(data map[string]any) (re.Rule, error) { - var r re.Rule - - id, ok := data["id"].(string) - if !ok { - return re.Rule{}, errID - } - r.ID = id - - name, ok := data["name"].(string) - if !ok { - return re.Rule{}, errName - } - r.Name = name - - stat, ok := data["status"].(string) - if !ok { - return re.Rule{}, errStatus - } - st, err := re.ToStatus(stat) - if err != nil { - return re.Rule{}, errors.Wrap(errConvertStatus, err) - } - r.Status = st - - cby, ok := data["created_by"].(string) - if !ok { - return re.Rule{}, errCreatedBy - } - r.CreatedBy = cby - - cat, ok := data["created_at"].(string) - if !ok { - return re.Rule{}, errCreatedAt - } - ct, err := time.Parse(re.TimeLayout, cat) - if err != nil { - return re.Rule{}, errors.Wrap(errCreatedAt, err) - } - r.CreatedAt = ct - - if domain, ok := data["domain"].(string); ok { - r.DomainID = domain - } - - if itags, ok := data["tags"].([]any); ok { - tags, err := toStrings(itags) - if err != nil { - return re.Rule{}, errors.Wrap(errTags, err) - } - r.Tags = tags - } - - if meta, ok := data["metadata"].(map[string]any); ok { - r.Metadata = meta - } - - if uby, ok := data["updated_by"].(string); ok { - r.UpdatedBy = uby - } - - if uat, ok := data["updated_at"].(string); ok { - ut, err := time.Parse(re.TimeLayout, uat) - if err != nil { - return re.Rule{}, errors.Wrap(errUpdatedAt, err) - } - r.UpdatedAt = ut - } - - if ic, ok := data["input_channel"].(string); ok { - r.InputChannel = ic - } - - if it, ok := data["input_topic"].(string); ok { - r.InputTopic = it - } - - if rawLogic, ok := data["logic"].(map[string]any); ok { - b, err := json.Marshal(rawLogic) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeLogic, err) - } - if err := json.Unmarshal(b, &r.Logic); err != nil { - return re.Rule{}, errors.Wrap(errDecodeLogic, err) - } - } - - if rawSched, ok := data["schedule"].(map[string]any); ok { - b, err := json.Marshal(rawSched) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeSchedule, err) - } - var sched schedule.Schedule - if err := json.Unmarshal(b, &sched); err != nil { - return re.Rule{}, errors.Wrap(errDecodeSchedule, err) - } - r.Schedule = sched - } - - return r, nil -} - -func toStrings(values []any) ([]string, error) { - strings := make([]string, 0, len(values)) - for _, value := range values { - str, ok := value.(string) - if !ok { - return nil, errStringValue - } - strings = append(strings, str) - } - return strings, nil -} - -func decodeAddRuleEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeAddRuleEvent, err) - } - - return r, nil -} - -func decodeUpdateRuleEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeUpdateRuleEvent, err) - } - return r, nil -} - -func decodeUpdateRuleTagsEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeUpdateRuleTagsEvent, err) - } - return r, nil -} - -func decodeUpdateRuleScheduleEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeUpdateRuleScheduleEvent, err) - } - return r, nil -} - -func decodeEnableRuleEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeEnableRuleEvent, err) - } - return r, nil -} - -func decodeDisableRuleEvent(data map[string]any) (re.Rule, error) { - r, err := ToRule(data) - if err != nil { - return re.Rule{}, errors.Wrap(errDecodeDisableRuleEvent, err) - } - return r, nil -} - -func decodeRemoveRuleEvent(data map[string]any) (string, error) { - id, ok := data["id"].(string) - if !ok { - return "", errors.Wrap(errDecodeRemoveRuleEvent, errID) - } - return id, nil -} diff --git a/pkg/re/events/consumer/doc.go b/pkg/re/events/consumer/doc.go deleted file mode 100644 index 581353cb3..000000000 --- a/pkg/re/events/consumer/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package consumer contains events consumer for events -// published by the Rules Engine service. -package consumer diff --git a/pkg/re/events/consumer/stream.go b/pkg/re/events/consumer/stream.go deleted file mode 100644 index 864320b89..000000000 --- a/pkg/re/events/consumer/stream.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package consumer - -import ( - "context" - "log/slog" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/events/store" - "github.com/absmach/magistrala/re" -) - -const ( - stream = "events.magistrala.rule.*" - - create = "rule.create" - update = "rule.update" - updateTags = "rule.update_tags" - updateSchedule = "rule.update_schedule" - enable = "rule.enable" - disable = "rule.disable" - remove = "rule.remove" -) - -var ( - errNoOperationKey = errors.New("operation key is not found in event message") - errAddRuleEvent = errors.New("failed to consume rule create event") - errUpdateRuleEvent = errors.New("failed to consume rule update event") - errUpdateRuleTagsEvent = errors.New("failed to consume rule update tags event") - errUpdateRuleScheduleEvent = errors.New("failed to consume rule update schedule event") - errEnableRuleEvent = errors.New("failed to consume rule enable event") - errDisableRuleEvent = errors.New("failed to consume rule disable event") - errRemoveRuleEvent = errors.New("failed to consume rule remove event") -) - -type eventHandler struct { - repo re.Repository -} - -func RulesEventsSubscribe(ctx context.Context, repo re.Repository, esURL, esConsumerName string, logger *slog.Logger) error { - subscriber, err := store.NewSubscriber(ctx, esURL, "re-es-sub", logger) - if err != nil { - return err - } - - subConfig := events.SubscriberConfig{ - Stream: stream, - Consumer: esConsumerName, - Handler: NewEventHandler(repo), - Ordered: true, - } - return subscriber.Subscribe(ctx, subConfig) -} - -// NewEventHandler returns new event store handler. -func NewEventHandler(repo re.Repository) events.EventHandler { - return &eventHandler{ - repo: repo, - } -} - -func (es *eventHandler) Handle(ctx context.Context, event events.Event) error { - msg, err := event.Encode() - if err != nil { - return err - } - - op, ok := msg["operation"] - if !ok { - return errNoOperationKey - } - - switch op { - case create: - return es.addRuleHandler(ctx, msg) - case update: - return es.updateRuleHandler(ctx, msg) - case updateTags: - return es.updateRuleTagsHandler(ctx, msg) - case updateSchedule: - return es.updateRuleScheduleHandler(ctx, msg) - case enable: - return es.enableRuleHandler(ctx, msg) - case disable: - return es.disableRuleHandler(ctx, msg) - case remove: - return es.removeRuleHandler(ctx, msg) - } - - return nil -} - -func (es *eventHandler) addRuleHandler(ctx context.Context, data map[string]any) error { - r, err := decodeAddRuleEvent(data) - if err != nil { - return errors.Wrap(errAddRuleEvent, err) - } - - if _, err := es.repo.AddRule(ctx, r); err != nil { - return errors.Wrap(errAddRuleEvent, err) - } - - return nil -} - -func (es *eventHandler) updateRuleHandler(ctx context.Context, data map[string]any) error { - r, err := decodeUpdateRuleEvent(data) - if err != nil { - return errors.Wrap(errUpdateRuleEvent, err) - } - - if _, err := es.repo.UpdateRule(ctx, r); err != nil { - return errors.Wrap(errUpdateRuleEvent, err) - } - - return nil -} - -func (es *eventHandler) updateRuleTagsHandler(ctx context.Context, data map[string]any) error { - r, err := decodeUpdateRuleTagsEvent(data) - if err != nil { - return errors.Wrap(errUpdateRuleTagsEvent, err) - } - - if _, err := es.repo.UpdateRuleTags(ctx, r); err != nil { - return errors.Wrap(errUpdateRuleTagsEvent, err) - } - - return nil -} - -func (es *eventHandler) updateRuleScheduleHandler(ctx context.Context, data map[string]any) error { - r, err := decodeUpdateRuleScheduleEvent(data) - if err != nil { - return errors.Wrap(errUpdateRuleScheduleEvent, err) - } - - if _, err := es.repo.UpdateRuleSchedule(ctx, r); err != nil { - return errors.Wrap(errUpdateRuleScheduleEvent, err) - } - - return nil -} - -func (es *eventHandler) enableRuleHandler(ctx context.Context, data map[string]any) error { - r, err := decodeEnableRuleEvent(data) - if err != nil { - return errors.Wrap(errEnableRuleEvent, err) - } - - if _, err := es.repo.UpdateRuleStatus(ctx, r); err != nil { - return errors.Wrap(errEnableRuleEvent, err) - } - - return nil -} - -func (es *eventHandler) disableRuleHandler(ctx context.Context, data map[string]any) error { - r, err := decodeDisableRuleEvent(data) - if err != nil { - return errors.Wrap(errDisableRuleEvent, err) - } - - if _, err := es.repo.UpdateRuleStatus(ctx, r); err != nil { - return errors.Wrap(errDisableRuleEvent, err) - } - - return nil -} - -func (es *eventHandler) removeRuleHandler(ctx context.Context, data map[string]any) error { - id, err := decodeRemoveRuleEvent(data) - if err != nil { - return errors.Wrap(errRemoveRuleEvent, err) - } - - if err := es.repo.RemoveRule(ctx, id); err != nil { - return errors.Wrap(errRemoveRuleEvent, err) - } - - return nil -} diff --git a/pkg/readersclient/client.go b/pkg/readersclient/client.go new file mode 100644 index 000000000..95791555e --- /dev/null +++ b/pkg/readersclient/client.go @@ -0,0 +1,70 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package readersclient provides the public gRPC client used to read messages +// from Community-owned reader services. +package readersclient + +import ( + "context" + "fmt" + "time" + + grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var _ grpcReadersV1.ReadersServiceClient = (*client)(nil) + +type client struct { + readers grpcReadersV1.ReadersServiceClient + timeout time.Duration +} + +// New returns a reader gRPC client with a per-request timeout. +func New(conn *grpc.ClientConn, timeout time.Duration) grpcReadersV1.ReadersServiceClient { + return &client{ + readers: grpcReadersV1.NewReadersServiceClient(conn), + timeout: timeout, + } +} + +func (c *client) ReadMessages(ctx context.Context, req *grpcReadersV1.ReadMessagesReq, opts ...grpc.CallOption) (*grpcReadersV1.ReadMessagesRes, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + res, err := c.readers.ReadMessages(ctx, req, opts...) + if err != nil { + return &grpcReadersV1.ReadMessagesRes{}, decodeError(err) + } + return res, nil +} + +func decodeError(err error) error { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.Unauthenticated: + return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message())) + case codes.PermissionDenied: + return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message())) + case codes.InvalidArgument, codes.FailedPrecondition: + return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) + case codes.NotFound: + return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message())) + case codes.AlreadyExists: + return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message())) + case codes.OK: + if msg := st.Message(); msg != "" { + return errors.Wrap(errors.ErrUnidentified, errors.New(msg)) + } + return nil + default: + return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message())) + } + } + return err +} diff --git a/pkg/readersclient/client_test.go b/pkg/readersclient/client_test.go new file mode 100644 index 000000000..de6b5d7da --- /dev/null +++ b/pkg/readersclient/client_test.go @@ -0,0 +1,141 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package readersclient + +import ( + "context" + "errors" + "testing" + "time" + + grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" + mgerrors "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type readersClientStub struct { + req *grpcReadersV1.ReadMessagesReq + res *grpcReadersV1.ReadMessagesRes + err error + hasDeadline bool +} + +func (stub *readersClientStub) ReadMessages(ctx context.Context, req *grpcReadersV1.ReadMessagesReq, _ ...grpc.CallOption) (*grpcReadersV1.ReadMessagesRes, error) { + stub.req = req + _, stub.hasDeadline = ctx.Deadline() + return stub.res, stub.err +} + +func TestReadMessages(t *testing.T) { + req := &grpcReadersV1.ReadMessagesReq{ + ChannelId: "channel", + DomainId: "domain", + PageMetadata: &grpcReadersV1.PageMetadata{ + Publishers: []string{"publisher-1", "publisher-2"}, + Order: "time", + Dir: "desc", + }, + } + expected := &grpcReadersV1.ReadMessagesRes{ + Total: 2, + PageMetadata: &grpcReadersV1.PageMetadata{ + Offset: 3, + Limit: 5, + Order: "time", + Dir: "desc", + }, + } + stub := &readersClientStub{res: expected} + client := &client{ + readers: stub, + timeout: time.Second, + } + + actual, err := client.ReadMessages(t.Context(), req) + + require.NoError(t, err) + assert.Same(t, req, stub.req) + assert.Same(t, expected, actual) + assert.True(t, stub.hasDeadline) +} + +func TestReadMessagesError(t *testing.T) { + stub := &readersClientStub{err: status.Error(codes.NotFound, "missing messages")} + client := &client{ + readers: stub, + timeout: time.Second, + } + + res, err := client.ReadMessages(t.Context(), &grpcReadersV1.ReadMessagesReq{}) + + require.Error(t, err) + assert.NotNil(t, res) + assert.True(t, mgerrors.Contains(err, svcerr.ErrNotFound)) +} + +func TestDecodeError(t *testing.T) { + native := errors.New("native error") + tests := []struct { + name string + input error + expected error + }{ + { + name: "unauthenticated", + input: status.Error(codes.Unauthenticated, "missing credentials"), + expected: svcerr.ErrAuthentication, + }, + { + name: "permission denied", + input: status.Error(codes.PermissionDenied, "forbidden"), + expected: svcerr.ErrAuthorization, + }, + { + name: "invalid argument", + input: status.Error(codes.InvalidArgument, "bad request"), + expected: mgerrors.ErrMalformedEntity, + }, + { + name: "failed precondition", + input: status.Error(codes.FailedPrecondition, "bad state"), + expected: mgerrors.ErrMalformedEntity, + }, + { + name: "not found", + input: status.Error(codes.NotFound, "missing"), + expected: svcerr.ErrNotFound, + }, + { + name: "already exists", + input: status.Error(codes.AlreadyExists, "duplicate"), + expected: svcerr.ErrConflict, + }, + { + name: "unknown status", + input: status.Error(codes.Unavailable, "offline"), + expected: errors.New("unexpected gRPC status"), + }, + { + name: "native error", + input: native, + expected: native, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := decodeError(tc.input) + if tc.name == "unknown status" { + assert.ErrorContains(t, actual, tc.expected.Error()) + return + } + assert.True(t, mgerrors.Contains(actual, tc.expected), "expected %q to contain %q", actual, tc.expected) + }) + } +} diff --git a/pkg/sdk/README.md b/pkg/sdk/README.md index ba7b25519..5a0a1fbf7 100644 --- a/pkg/sdk/README.md +++ b/pkg/sdk/README.md @@ -2,7 +2,7 @@ Go SDK, a Go driver for Magistrala HTTP API. -Provides comprehensive functionality for system administration (provisioning), messaging, user management, domain management, groups, channels, clients, certificates, invitations, and journal operations. +Provides comprehensive functionality for system administration (provisioning), messaging, user management, domain management, groups, channels, clients, certificates and invitations. ## Installation @@ -31,7 +31,6 @@ func main() { DomainsURL: "http://localhost:8189", HTTPAdapterURL: "http://localhost:8008", CertsURL: "http://localhost:9019", - JournalURL: "http://localhost:9021", HostURL: "http://localhost", } @@ -84,7 +83,6 @@ type Config struct { GroupsURL string ChannelsURL string DomainsURL string - JournalURL string HostURL string MsgContentType ContentType TLSVerification bool @@ -348,13 +346,6 @@ RejectInvitation(ctx context.Context, domainID, token string) error DeleteInvitation(ctx context.Context, userID, domainID, token string) error ``` -### Journal Management - -```go -// Get journal logs -Journal(ctx context.Context, entityType, entityID, domainID string, pm PageMetadata, token string) (JournalsPage, error) -``` - ### Messaging ```go diff --git a/pkg/sdk/health.go b/pkg/sdk/health.go index ba2348065..1ba5a0d60 100644 --- a/pkg/sdk/health.go +++ b/pkg/sdk/health.go @@ -44,8 +44,6 @@ func (sdk mgSDK) Health(service string) (HealthInfo, errors.SDKError) { url = fmt.Sprintf("%s/health", sdk.channelsURL) case "domains": url = fmt.Sprintf("%s/health", sdk.domainsURL) - case "journal": - url = fmt.Sprintf("%s/health", sdk.journalURL) case "fluxmq": url = fmt.Sprintf("%s/health", sdk.httpAdapterURL) } diff --git a/pkg/sdk/journal.go b/pkg/sdk/journal.go deleted file mode 100644 index 7d14c8fb7..000000000 --- a/pkg/sdk/journal.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package sdk - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/pkg/errors" -) - -const journalEndpoint = "journal" - -type Journal struct { - ID string `json:"id,omitempty"` - Operation string `json:"operation,omitempty"` - OccurredAt time.Time `json:"occurred_at,omitempty"` - Attributes Metadata `json:"attributes,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` -} - -type JournalsPage struct { - Total uint64 `json:"total"` - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Journals []Journal `json:"journals"` -} - -func (sdk mgSDK) Journal(ctx context.Context, entityType, entityID, domainID string, pm PageMetadata, token string) (journals JournalsPage, err error) { - if entityID == "" { - return JournalsPage{}, errors.NewSDKError(apiutil.ErrMissingID) - } - if entityType == "" { - return JournalsPage{}, errors.NewSDKError(apiutil.ErrMissingEntityType) - } - - reqUrl := fmt.Sprintf("%s/%s/%s/%s", domainID, journalEndpoint, entityType, entityID) - if entityType == "user" { - reqUrl = fmt.Sprintf("%s/%s/%s", journalEndpoint, entityType, entityID) - } - - url, err := sdk.withQueryParams(sdk.journalURL, reqUrl, pm) - if err != nil { - return JournalsPage{}, errors.NewSDKError(err) - } - - _, body, sdkErr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK) - if sdkErr != nil { - return JournalsPage{}, sdkErr - } - - var journalsPage JournalsPage - if err := json.Unmarshal(body, &journalsPage); err != nil { - return JournalsPage{}, errors.NewSDKError(err) - } - - return journalsPage, nil -} diff --git a/pkg/sdk/mocks/sdk.go b/pkg/sdk/mocks/sdk.go index 382dcd25f..73b0f97b4 100644 --- a/pkg/sdk/mocks/sdk.go +++ b/pkg/sdk/mocks/sdk.go @@ -7774,96 +7774,6 @@ func (_c *SDK_IssueFromCSRInternal_Call) RunAndReturn(run func(ctx context.Conte return _c } -// Journal provides a mock function for the type SDK -func (_mock *SDK) Journal(ctx context.Context, entityType string, entityID string, domainID string, pm sdk.PageMetadata, token string) (sdk.JournalsPage, error) { - ret := _mock.Called(ctx, entityType, entityID, domainID, pm, token) - - if len(ret) == 0 { - panic("no return value specified for Journal") - } - - var r0 sdk.JournalsPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, sdk.PageMetadata, string) (sdk.JournalsPage, error)); ok { - return returnFunc(ctx, entityType, entityID, domainID, pm, token) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, sdk.PageMetadata, string) sdk.JournalsPage); ok { - r0 = returnFunc(ctx, entityType, entityID, domainID, pm, token) - } else { - r0 = ret.Get(0).(sdk.JournalsPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, sdk.PageMetadata, string) error); ok { - r1 = returnFunc(ctx, entityType, entityID, domainID, pm, token) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// SDK_Journal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Journal' -type SDK_Journal_Call struct { - *mock.Call -} - -// Journal is a helper method to define mock.On call -// - ctx context.Context -// - entityType string -// - entityID string -// - domainID string -// - pm sdk.PageMetadata -// - token string -func (_e *SDK_Expecter) Journal(ctx interface{}, entityType interface{}, entityID interface{}, domainID interface{}, pm interface{}, token interface{}) *SDK_Journal_Call { - return &SDK_Journal_Call{Call: _e.mock.On("Journal", ctx, entityType, entityID, domainID, pm, token)} -} - -func (_c *SDK_Journal_Call) Run(run func(ctx context.Context, entityType string, entityID string, domainID string, pm sdk.PageMetadata, token string)) *SDK_Journal_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - var arg3 string - if args[3] != nil { - arg3 = args[3].(string) - } - var arg4 sdk.PageMetadata - if args[4] != nil { - arg4 = args[4].(sdk.PageMetadata) - } - var arg5 string - if args[5] != nil { - arg5 = args[5].(string) - } - run( - arg0, - arg1, - arg2, - arg3, - arg4, - arg5, - ) - }) - return _c -} - -func (_c *SDK_Journal_Call) Return(journal sdk.JournalsPage, err error) *SDK_Journal_Call { - _c.Call.Return(journal, err) - return _c -} - -func (_c *SDK_Journal_Call) RunAndReturn(run func(ctx context.Context, entityType string, entityID string, domainID string, pm sdk.PageMetadata, token string) (sdk.JournalsPage, error)) *SDK_Journal_Call { - _c.Call.Return(run) - return _c -} - // ListAlarms provides a mock function for the type SDK func (_mock *SDK) ListAlarms(ctx context.Context, pm sdk.PageMetadata, domainID string, token string) (sdk.AlarmsPage, errors.SDKError) { ret := _mock.Called(ctx, pm, domainID, token) diff --git a/pkg/sdk/sdk.go b/pkg/sdk/sdk.go index 544b50b1d..9d65299aa 100644 --- a/pkg/sdk/sdk.go +++ b/pkg/sdk/sdk.go @@ -1597,14 +1597,6 @@ type SDK interface { // fmt.Println(err) DeleteInvitation(ctx context.Context, userID, domainID, token string) (err error) - // Journal returns a list of journal logs. - // - // For example: - // ctx := context.Background() - // journals, _ := sdk.Journal(ctx, "client", "clientID","domainID", PageMetadata{Offset: 0, Limit: 10, Operation: "client.create"}, "token") - // fmt.Println(journals) - Journal(ctx context.Context, entityType, entityID, domainID string, pm PageMetadata, token string) (journal JournalsPage, err error) - // DomainInvitations returns a list of invitations for a specific domain. // For example: // ctx := context.Background() @@ -1862,7 +1854,6 @@ type mgSDK struct { groupsURL string channelsURL string domainsURL string - journalURL string HostURL string bootstrapURL string readersURL string @@ -1885,7 +1876,6 @@ type Config struct { GroupsURL string ChannelsURL string DomainsURL string - JournalURL string HostURL string BootstrapURL string ReaderURL string @@ -1909,7 +1899,6 @@ func NewSDK(conf Config) SDK { groupsURL: conf.GroupsURL, channelsURL: conf.ChannelsURL, domainsURL: conf.DomainsURL, - journalURL: conf.JournalURL, HostURL: conf.HostURL, bootstrapURL: conf.BootstrapURL, readersURL: conf.ReaderURL, diff --git a/internal/testsutil/common.go b/pkg/testsutil/common.go similarity index 100% rename from internal/testsutil/common.go rename to pkg/testsutil/common.go diff --git a/internal/testsutil/redis.go b/pkg/testsutil/redis.go similarity index 100% rename from internal/testsutil/redis.go rename to pkg/testsutil/redis.go diff --git a/re/README.md b/re/README.md deleted file mode 100644 index 4fff510e0..000000000 --- a/re/README.md +++ /dev/null @@ -1,320 +0,0 @@ -# Rules Engine - -The Magistrala Rules Engine (RE) processes incoming messages using user-defined scripts (Lua or Go) and routes the results to outputs such as channels, alarms, email, SenML writers, PostgreSQL, or Slack. It also supports scheduled rule execution and publishes rule events to the event store. - -## Configuration - -The service is configured using the following environment variables (values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) as consumed by [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml)): - -### Core service - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_RE_LOG_LEVEL` | Log level for the service | `debug` | -| `MG_RE_HTTP_HOST` | HTTP host to bind | `re` | -| `MG_RE_HTTP_PORT` | HTTP port to bind | `9008` | -| `MG_RE_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" | -| `MG_RE_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" | -| `MG_RE_INSTANCE_ID` | Instance ID for tracing/health | "" | -| `MG_MESSAGE_BROKER_URL` | Internal message broker URL | `nats://nats:4222` | -| `MG_ES_URL` | Event store broker URL | `nats://nats:4222` | -| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` | -| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` | -| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` | - -### Database - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_RE_DB_HOST` | PostgreSQL host | `re-db` | -| `MG_RE_DB_PORT` | PostgreSQL port | `5432` | -| `MG_RE_DB_USER` | PostgreSQL user | `magistrala` | -| `MG_RE_DB_PASS` | PostgreSQL password | `magistrala` | -| `MG_RE_DB_NAME` | PostgreSQL database name | `rules_engine` | -| `MG_RE_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` | -| `MG_RE_DB_SSL_CERT` | PostgreSQL SSL client cert | "" | -| `MG_RE_DB_SSL_KEY` | PostgreSQL SSL client key | "" | -| `MG_RE_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" | - -### Atom - -| Variable | Description | Default | -| --- | --- | --- | -| `ATOM_URL` | Atom HTTP endpoint | `http://atom:8080` | -| `ATOM_JWKS_URL` | Atom JWKS endpoint for JWT verification | `http://atom:8080/.well-known/jwks.json` | -| `ATOM_ADMIN_USERNAME` | Atom admin login for service projections | `atom-admin` | -| `ATOM_ADMIN_SECRET` | Atom admin secret for service projections | `change-me` | -| `ATOM_TIMEOUT` | Atom request timeout | `5s` | -| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` | - -### Readers gRPC - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_TIMESCALE_READER_GRPC_URL` | Readers gRPC endpoint | `timescale-reader:7011` | -| `MG_TIMESCALE_READER_GRPC_TIMEOUT` | Readers gRPC timeout | `300s` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_CERT` | Readers gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/reader-grpc-client.crt}` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_CA_CERTS` | Readers gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_KEY` | Readers gRPC client key path | `${GRPC_MTLS:+./ssl/certs/readers-grpc-client.key}` | - -### Email - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_EMAIL_HOST` | SMTP host | `smtp.mailtrap.io` | -| `MG_EMAIL_PORT` | SMTP port | `2525` | -| `MG_EMAIL_USERNAME` | SMTP username | `18bf7f70705139` | -| `MG_EMAIL_PASSWORD` | SMTP password | `2b0d302e775b1e` | -| `MG_EMAIL_FROM_ADDRESS` | Sender email address | `from@example.com` | -| `MG_EMAIL_FROM_NAME` | Sender display name | `Example` | -| `MG_EMAIL_TEMPLATE` | Email template path | `email.tmpl` | -| `MG_RE_EMAIL_TEMPLATE` | Template file mounted by Docker Compose | `re.tmpl` | - -### Callout - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_RE_CALLOUT_URLS` | Callout target URLs | "" | -| `MG_RE_CALLOUT_METHOD` | Callout HTTP method | `POST` | -| `MG_RE_CALLOUT_TLS_VERIFICATION` | TLS verification for callout | `false` | -| `MG_RE_CALLOUT_TIMEOUT` | Callout timeout | `10s` | -| `MG_RE_CALLOUT_CA_CERT` | Callout CA cert path | "" | -| `MG_RE_CALLOUT_CERT` | Callout client cert path | "" | -| `MG_RE_CALLOUT_KEY` | Callout client key path | "" | -| `MG_RE_CALLOUT_OPERATIONS` | Callout operations filter | "" | - -### Optional cache defaults (from code) - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_RE_CACHE_URL` | Cache URL | `redis://localhost:6379/0` | -| `MG_RE_CACHE_KEY_DURATION` | Cache key TTL | `10m` | - -## Features - -- **Rule execution**: Runs Lua or Go scripts for incoming messages. -- **Multiple outputs**: Channels, alarms, email, SenML writers, remote PostgreSQL, and Slack outputs. -- **Scheduling**: Runs rules at specific times with recurring intervals. -- **Filtering and matching**: Input channel filtering and MQTT-style topic matching (`+`, `#`). -- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support. -- **Payload limit**: Messages over 100 kB are rejected for processing. - -## Architecture - -### Runtime flow - -1. The service subscribes to all internal broker messages. -2. For each message, it lists enabled rules for the same domain and input channel. -3. It matches the rule `input_topic` against the message subtopic using MQTT-style wildcards. -4. The rule logic (Lua or Go) is executed and the result is passed to configured outputs. - -### Message payloads - -In Lua, the engine injects a global `message` object: - -```lua -message = { - domain = "domain_id", - channel = "channel_id", - subtopic = "subtopic", - publisher = "client_id", - protocol = "nats", - created = timestamp, - payload = { ... } -- JSON object/array or a byte array if payload is not JSON -} -``` - -For Go scripts, the message is exposed as `messaging/m.message` and `main.logicFunction` must return a value. - -In rule definitions, `logic.type` uses numeric values: `0` = Lua, `1` = Go. - -If a script returns `false`, outputs are skipped. - -### Scheduling - -The scheduler runs on a 30-second ticker and selects enabled rules with a due time (`time`) earlier than now. It updates the next due time using `Schedule.NextDue()` and executes each rule with a synthetic message containing the scheduled timestamp. - -Recurring types are: `none`, `hourly`, `daily`, `weekly`, `monthly`. The `recurring_period` controls the interval (1 = every interval, 2 = every second interval, etc.). - -### Outputs - -Supported output types (`outputs.OutputType`) and their fields: - -| Output type | Fields | Notes | -| --- | --- | --- | -| `channels` | `channel`, `topic` | Republish result to another channel/topic. | -| `alarms` | none | Emits alarms from the script result. | -| `save_senml` | none | Forwards SenML to writers. | -| `email` | `to`, `subject`, `content` | `content` is a Go template. | -| `save_remote_pg` | `host`, `port`, `user`, `password`, `database`, `table`, `mapping` | `mapping` is a Go template that must render a JSON object. | -| `slack` | `token`, `channel_id`, `message` | `message` is a Go template. | - -For `channels` output, `topic` is a slash-delimited subtopic (for example, `alerts/high-temp`). - -Templates receive a `Message` (the incoming message) and a `Result` (the script output) value. - -## Data model - -### Rules table - -Defined in `re/postgres/init.go`: - -| Column | Type | Description | -| --- | --- | --- | -| `id` | `VARCHAR(36)` | Rule UUID (primary key) | -| `name` | `VARCHAR(1024)` | Rule name | -| `domain_id` | `VARCHAR(36)` | Domain ID | -| `metadata` | `JSONB` | Custom metadata | -| `tags` | `TEXT[]` | Rule tags | -| `created_by` | `VARCHAR(254)` | Creator user ID | -| `created_at` | `TIMESTAMP` | Creation timestamp | -| `updated_at` | `TIMESTAMP` | Last update timestamp | -| `updated_by` | `VARCHAR(254)` | Last updater user ID | -| `input_channel` | `VARCHAR(36)` | Input channel ID | -| `input_topic` | `TEXT` | Input topic (supports `+` and `#` wildcards) | -| `outputs` | `JSONB` | Output definitions | -| `status` | `SMALLINT` | 0 = enabled, 1 = disabled, 2 = deleted | -| `logic_type` | `SMALLINT` | 0 = Lua, 1 = Go | -| `logic_value` | `BYTEA` | Script body | -| `start_datetime` | `TIMESTAMP` | Schedule start time | -| `time` | `TIMESTAMP` | Next scheduled execution time | -| `recurring` | `SMALLINT` | Recurring type | -| `recurring_period` | `SMALLINT` | Recurring period | - -## Deployment - -### Build and run locally - -```bash -make re - -MG_RE_LOG_LEVEL=debug \ -MG_RE_HTTP_PORT=9008 \ -MG_RE_DB_HOST=localhost \ -MG_RE_DB_PORT=5432 \ -MG_RE_DB_USER=magistrala \ -MG_RE_DB_PASS=magistrala \ -MG_RE_DB_NAME=rules_engine \ -MG_MESSAGE_BROKER_URL=nats://localhost:4222 \ -MG_ES_URL=nats://localhost:4222 \ -MG_AUTH_GRPC_URL=localhost:7001 \ -MG_AUTH_GRPC_TIMEOUT=300s \ -MG_DOMAINS_GRPC_URL=localhost:7003 \ -MG_DOMAINS_GRPC_TIMEOUT=300s \ -MG_TIMESCALE_READER_GRPC_URL=localhost:7011 \ -MG_TIMESCALE_READER_GRPC_TIMEOUT=300s \ -./build/re -``` - -### Docker Compose - -The service is available as a Docker container. Refer to [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) for the `re` and `re-db` services and their environment variables. For a full local stack, ensure auth, domains, readers, and the message broker are running. - -```bash -docker compose -f docker/docker-compose.yaml up re re-db -``` - -### Health check - -```bash -curl -X GET http://localhost:9008/health \ - -H "accept: application/health+json" -``` - -## Testing - -```bash -go test ./re/... -``` - -## Usage - -The Rules Engine service supports the following operations: - -| Operation | Method & Path | Description | -| --- | --- | --- | -| `createRule` | `POST /{domainID}/rules` | Create a new rule | -| `listRules` | `GET /{domainID}/rules` | List rules with filters | -| `viewRule` | `GET /{domainID}/rules/{ruleID}` | Retrieve a rule | -| `updateRule` | `PATCH /{domainID}/rules/{ruleID}` | Update a rule | -| `updateRuleTags` | `PATCH /{domainID}/rules/{ruleID}/tags` | Update rule tags | -| `updateRuleSchedule` | `PATCH /{domainID}/rules/{ruleID}/schedule` | Update rule schedule | -| `enableRule` | `POST /{domainID}/rules/{ruleID}/enable` | Enable a rule | -| `disableRule` | `POST /{domainID}/rules/{ruleID}/disable` | Disable a rule | -| `removeRule` | `DELETE /{domainID}/rules/{ruleID}` | Delete a rule | -| `health` | `GET /health` | Service health check | - -List filters: `offset`, `limit`, `name`, `input_channel`, `status`, `order` (`name`, `created_at`, `updated_at`), `dir` (`asc`, `desc`), and `tag`. - -### Example: Create a rule (Lua + alarms + channels) - -```bash -curl -X POST http://localhost:9008//rules \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "High Temperature Alert", - "input_channel": "sensors", - "input_topic": "temperature/+", - "logic": { - "type": 0, - "value": "if message.payload.t > 30 then return {measurement=\"temperature\", value=tostring(message.payload.t), unit=\"C\", threshold=\"30\", cause=\"temp high\", severity=90} end" - }, - "outputs": [ - { "type": "alarms" }, - { "type": "channels", "channel": "alerts", "topic": "temperature" } - ], - "tags": ["temp", "alerts"], - "metadata": { "site": "lab" } - }' -``` - -### Example: List rules - -```bash -curl -X GET "http://localhost:9008//rules?status=enabled&input_channel=sensors&order=updated_at&dir=desc&tag=temp" \ - -H "Authorization: Bearer " -``` - -### Example: Update rule tags - -```bash -curl -X PATCH http://localhost:9008//rules//tags \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ "tags": ["temp", "critical"] }' -``` - -### Example: Update rule schedule - -```bash -curl -X PATCH http://localhost:9008//rules//schedule \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "schedule": { - "start_datetime": "2025-01-01T00:00:00Z", - "time": "2025-01-01T00:00:00Z", - "recurring": "hourly", - "recurring_period": 1 - } - }' -``` - -### Example: Enable a rule - -```bash -curl -X POST http://localhost:9008//rules//enable \ - -H "Authorization: Bearer " -``` - -### Example: Delete a rule - -```bash -curl -X DELETE http://localhost:9008//rules/ \ - -H "Authorization: Bearer " -``` - -For an in-depth explanation of our Rules Engine Service, see the [official documentation][doc]. - -[doc]: https://magistrala.absmach.eu/docs/dev-guide/services/rules-engine/ diff --git a/re/api/doc.go b/re/api/doc.go deleted file mode 100644 index 2424852cc..000000000 --- a/re/api/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package api contains API-related concerns: endpoint definitions, middlewares -// and all resource representations. -package api diff --git a/re/api/endpoints.go b/re/api/endpoints.go deleted file mode 100644 index 04f2442f0..000000000 --- a/re/api/endpoints.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/re" - "github.com/go-kit/kit/endpoint" -) - -func addRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(addRuleReq) - if err := req.validate(); err != nil { - return addRuleRes{}, err - } - rule, err := s.AddRule(ctx, session, req.Rule) - if err != nil { - return addRuleRes{}, err - } - return addRuleRes{Rule: rule, created: true}, nil - } -} - -func viewRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(viewRuleReq) - if err := req.validate(); err != nil { - return viewRuleRes{}, err - } - rule, err := s.ViewRule(ctx, session, req.id, req.withRoles) - if err != nil { - return viewRuleRes{}, err - } - return viewRuleRes{Rule: rule}, nil - } -} - -func updateRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateRuleReq) - if err := req.validate(); err != nil { - return updateRuleRes{}, err - } - rule, err := s.UpdateRule(ctx, session, req.Rule) - if err != nil { - return updateRuleRes{}, err - } - return updateRuleRes{Rule: rule}, nil - } -} - -func updateRuleTagsEndpoint(svc re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - req := request.(updateRuleTagsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthentication - } - - r := re.Rule{ - ID: req.id, - Tags: req.Tags, - } - res, err := svc.UpdateRuleTags(ctx, session, r) - if err != nil { - return nil, err - } - - return updateRuleRes{Rule: res}, nil - } -} - -func updateRuleScheduleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateRuleScheduleReq) - if err := req.validate(); err != nil { - return updateRuleRes{}, err - } - - rule := re.Rule{ - ID: req.id, - Schedule: req.Schedule, - } - - updatedRule, err := s.UpdateRuleSchedule(ctx, session, rule) - if err != nil { - return updateRuleRes{}, err - } - return updateRuleRes{Rule: updatedRule}, nil - } -} - -func listRulesEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(listRulesReq) - if err := req.validate(); err != nil { - return pageRes{}, err - } - page, err := s.ListRules(ctx, session, req.PageMeta) - if err != nil { - return rulesPageRes{}, err - } - ret := rulesPageRes{ - Page: page, - } - return ret, nil - } -} - -func deleteRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(deleteRuleReq) - if err := req.validate(); err != nil { - return deleteRuleRes{}, err - } - err := s.RemoveRule(ctx, session, req.id) - if err != nil { - return deleteRuleRes{false}, err - } - return deleteRuleRes{true}, nil - } -} - -func enableRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateRuleStatusReq) - if err := req.validate(); err != nil { - return updateRuleStatusRes{}, err - } - - rule, err := s.EnableRule(ctx, session, req.id) - if err != nil { - return updateRuleStatusRes{}, err - } - - return updateRuleStatusRes{Rule: rule}, err - } -} - -func disableRuleEndpoint(s re.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateRuleStatusReq) - if err := req.validate(); err != nil { - return updateRuleStatusRes{}, err - } - - rule, err := s.DisableRule(ctx, session, req.id) - if err != nil { - return updateRuleStatusRes{}, err - } - - return updateRuleStatusRes{Rule: rule}, err - } -} diff --git a/re/api/endpoints_test.go b/re/api/endpoints_test.go deleted file mode 100644 index 5bfbfd9c2..000000000 --- a/re/api/endpoints_test.go +++ /dev/null @@ -1,1284 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api_test - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/testsutil" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkgSch "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/api" - "github.com/absmach/magistrala/re/mocks" - "github.com/go-chi/chi/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const contentType = "application/json" - -var ( - namegen = namegenerator.NewGenerator() - domainID = testsutil.GenerateUUID(&testing.T{}) - userID = testsutil.GenerateUUID(&testing.T{}) - validID = testsutil.GenerateUUID(&testing.T{}) - validToken = "valid" - invalidToken = "invalid" - now = time.Now().UTC().Truncate(time.Minute) - future = now.Add(1 * time.Hour) - schedule = pkgSch.Schedule{ - StartDateTime: future, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - } - rule = re.Rule{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Schedule: schedule, - Metadata: re.Metadata{ - "name": "test", - }, - } - past = now.Add(-1 * time.Hour) - scheduleInPast = pkgSch.Schedule{ - StartDateTime: past, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: past, - } -) - -type testRequest struct { - client *http.Client - method string - url string - contentType string - token string - body io.Reader -} - -func (tr testRequest) make() (*http.Response, error) { - req, err := http.NewRequest(tr.method, tr.url, tr.body) - if err != nil { - return nil, err - } - - if tr.token != "" { - req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token) - } - - if tr.contentType != "" { - req.Header.Set("Content-Type", tr.contentType) - } - - req.Header.Set("Referer", "http://localhost") - - return tr.client.Do(req) -} - -func newRuleEngineServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { - svc := new(mocks.Service) - authn := new(authnmocks.Authentication) - - logger := mglog.NewMock() - mux := chi.NewRouter() - am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true)) - - api.MakeHandler(svc, am, mux, logger, "") - - return httptest.NewServer(mux), svc, authn -} - -func toJSON(data any) string { - jsonData, err := json.Marshal(data) - if err != nil { - return "" - } - return string(jsonData) -} - -func TestAddRuleEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - ruleInPast := rule - ruleInPast.Schedule = scheduleInPast - - cases := []struct { - desc string - rule re.Rule - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes re.Rule - svcErr error - err error - len int - }{ - { - desc: "add rule successfully", - rule: rule, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusCreated, - svcRes: rule, - }, - { - desc: "add rule with invalid token", - rule: rule, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "add rule with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - rule: rule, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "add rule with name that is too long", - token: validToken, - rule: re.Rule{ - ID: validID, - Name: strings.Repeat("a", 1025), - Logic: re.Script{ - Type: re.ScriptType(0), - Value: "return `test` end", - }, - }, - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrNameSize, - }, - { - desc: "add rule with empty domainID", - token: validToken, - rule: rule, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "add rule with invalid content type", - token: validToken, - domainID: domainID, - rule: rule, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "add rule with startdatetime in past", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - rule: ruleInPast, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "add rule with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - rule: rule, - contentType: contentType, - svcErr: svcerr.ErrCreateEntity, - status: http.StatusUnprocessableEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.rule) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/rules", ts.URL, tc.domainID), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("AddRule", mock.Anything, tc.authnRes, tc.rule).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestViewRuleEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - id string - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes re.Rule - svcErr error - err error - len int - }{ - { - desc: "view rule successfully", - id: rule.ID, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusOK, - svcRes: rule, - }, - { - desc: "view rule with invalid token", - id: rule.ID, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "view rule with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - id: rule.ID, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "view rule with empty domainID", - token: validToken, - id: rule.ID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "view rule with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - id: rule.ID, - contentType: contentType, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/%s/rules/%s", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("ViewRule", mock.Anything, tc.authnRes, tc.id, false).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestListRulesEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - query string - domainID string - token string - session smqauthn.Session - listRulesResponse re.Page - status int - authnErr error - err error - }{ - { - desc: "list rules successfully", - domainID: domainID, - token: validToken, - status: http.StatusOK, - listRulesResponse: re.Page{ - Total: 1, - Rules: []re.Rule{rule}, - }, - err: nil, - }, - { - desc: "list rules with empty token", - domainID: domainID, - token: "", - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "list rules with invalid token", - domainID: domainID, - token: invalidToken, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "list rules with offset", - domainID: domainID, - token: validToken, - listRulesResponse: re.Page{ - Total: 1, - - Rules: []re.Rule{rule}, - }, - query: "offset=1", - status: http.StatusOK, - err: nil, - }, - { - desc: "list rules with invalid offset", - domainID: domainID, - token: validToken, - query: "offset=invalid", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidQueryParams, - }, - { - desc: "list rules with limit", - domainID: domainID, - token: validToken, - listRulesResponse: re.Page{ - Total: 1, - - Rules: []re.Rule{rule}, - }, - query: "limit=1", - status: http.StatusOK, - err: nil, - }, - { - desc: "list rules with invalid limit", - domainID: domainID, - token: validToken, - query: "limit=invalid", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidQueryParams, - }, - { - desc: "list rules with invalid direction", - domainID: domainID, - token: validToken, - query: "dir=invalid", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidDirection, - }, - { - desc: "list rules with invalid order", - domainID: domainID, - token: validToken, - query: "order=invalid", - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "list rule with limit that is too big", - domainID: domainID, - token: validToken, - query: "limit=10000", - status: http.StatusBadRequest, - err: apiutil.ErrLimitSize, - }, - { - desc: "list rules with input channel", - domainID: domainID, - token: validToken, - listRulesResponse: re.Page{ - Total: 1, - Rules: []re.Rule{rule}, - }, - query: "input_channel=input.channel", - status: http.StatusOK, - err: nil, - }, - { - desc: "list rules with duplicate input_channel", - domainID: domainID, - token: validToken, - query: "input_channel=1&input_channel=2", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidQueryParams, - }, - { - desc: "list rules with status", - domainID: domainID, - token: validToken, - listRulesResponse: re.Page{ - Total: 1, - Rules: []re.Rule{rule}, - }, - query: "status=enabled", - status: http.StatusOK, - err: nil, - }, - { - desc: "list rules with invalid status", - domainID: domainID, - token: validToken, - query: "status=invalid", - status: http.StatusBadRequest, - err: svcerr.ErrInvalidStatus, - }, - { - desc: "list rules with duplicate status", - domainID: domainID, - token: validToken, - query: "status=enabled&status=disabled", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidQueryParams, - }, - { - desc: "list rules with duplicate tags", - domainID: domainID, - token: validToken, - query: "tag=tag1&tag=tag2", - status: http.StatusBadRequest, - err: apiutil.ErrInvalidQueryParams, - }, - { - desc: "list rules with service error", - domainID: domainID, - token: validToken, - listRulesResponse: re.Page{}, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: ts.URL + "/" + tc.domainID + "/rules?" + tc.query, - contentType: contentType, - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("ListRules", mock.Anything, tc.session, mock.Anything).Return(tc.listRulesResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var bodyRes respBody - err = json.NewDecoder(res.Body).Decode(&bodyRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if bodyRes.Err != "" || bodyRes.Message != "" { - err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestUpdateRulesEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - updateRuleReq := re.Rule{ - ID: rule.ID, - Name: rule.Name, - Logic: re.Script{ - Type: re.ScriptType(0), - Value: "return `test` end", - }, - InputChannel: testsutil.GenerateUUID(&testing.T{}), - Metadata: map[string]any{ - "name": "test", - }, - } - - cases := []struct { - desc string - token string - id string - domainID string - updateReq re.Rule - contentType string - session smqauthn.Session - svcResp re.Rule - svcErr error - status int - authnErr error - err error - }{ - { - desc: "update rule successfully", - token: validToken, - domainID: domainID, - id: rule.ID, - updateReq: updateRuleReq, - contentType: contentType, - svcResp: rule, - status: http.StatusOK, - err: nil, - }, - { - desc: "update rule with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: rule.ID, - updateReq: updateRuleReq, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "update rule with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: rule.ID, - updateReq: updateRuleReq, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "update rule with empty domainID", - token: validToken, - id: rule.ID, - updateReq: updateRuleReq, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "update rule with name that is too long", - token: validToken, - id: validID, - domainID: domainID, - updateReq: re.Rule{ - ID: validID, - Name: strings.Repeat("a", 1025), - Logic: re.Script{ - Type: re.ScriptType(0), - Value: "return `test` end", - }, - }, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrNameSize, - }, - { - desc: "update rule with invalid content type", - token: validToken, - id: rule.ID, - domainID: domainID, - updateReq: updateRuleReq, - contentType: "application/xml", - svcResp: rule, - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "update rule with service error", - token: validToken, - id: rule.ID, - domainID: domainID, - updateReq: updateRuleReq, - contentType: contentType, - svcResp: re.Rule{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.updateReq) - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/%s/rules/%s", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("UpdateRule", mock.Anything, tc.session, tc.updateReq).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestUpdateRuleTagsEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - newTag := "newtag" - - cases := []struct { - desc string - token string - id string - domainID string - data string - contentType string - session smqauthn.Session - svcResp re.Rule - svcErr error - resp re.Rule - status int - authnErr error - err error - }{ - { - desc: "update rule tags successfully", - token: validToken, - domainID: domainID, - id: validID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: contentType, - svcResp: rule, - status: http.StatusOK, - err: nil, - }, - { - desc: "update rule tags with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "update rule tags with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "update rule tags with empty domainID", - token: validToken, - id: validID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "update rule tags with invalid content type", - token: validToken, - id: validID, - domainID: domainID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: "application/xml", - svcResp: rule, - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "update rule tags with service error", - token: validToken, - id: validID, - domainID: domainID, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - contentType: contentType, - svcResp: re.Rule{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "update rule with malformed request", - token: validToken, - id: validID, - domainID: domainID, - contentType: contentType, - data: fmt.Sprintf(`{"tags":["%s"}`, newTag), - status: http.StatusBadRequest, - err: apiutil.ErrMalformedRequestBody, - }, - { - desc: "update rule with empty id", - token: validToken, - id: "", - domainID: domainID, - contentType: contentType, - data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/%s/rules/%s/tags", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("UpdateRuleTags", mock.Anything, tc.session, re.Rule{ID: tc.id, Tags: []string{newTag}}).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestUpdateRuleScheduleEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - updateScheduleReq := pkgSch.Schedule{ - StartDateTime: future, - Time: future.Add(2 * time.Hour), - Recurring: pkgSch.Weekly, - RecurringPeriod: 2, - } - - ruleWithSchedule := rule - ruleWithSchedule.Schedule = updateScheduleReq - - cases := []struct { - desc string - token string - id string - domainID string - schedule pkgSch.Schedule - contentType string - session smqauthn.Session - svcResp re.Rule - svcErr error - status int - authnErr error - err error - }{ - { - desc: "update rule schedule successfully", - token: validToken, - domainID: domainID, - id: validID, - schedule: updateScheduleReq, - contentType: contentType, - svcResp: ruleWithSchedule, - status: http.StatusOK, - err: nil, - }, - { - desc: "update rule schedule with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - schedule: updateScheduleReq, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "update rule schedule with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - schedule: updateScheduleReq, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "update rule schedule with empty domainID", - token: validToken, - id: validID, - schedule: updateScheduleReq, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "update rule schedule with invalid content type", - token: validToken, - id: validID, - domainID: domainID, - schedule: updateScheduleReq, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "update rule schedule with start_datetime in past", - token: validToken, - id: validID, - domainID: domainID, - schedule: pkgSch.Schedule{ - StartDateTime: past, - Time: future, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - }, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "update rule schedule with service error", - token: validToken, - id: validID, - domainID: domainID, - schedule: updateScheduleReq, - contentType: contentType, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "update rule schedule with empty id", - token: validToken, - id: "", - domainID: domainID, - schedule: updateScheduleReq, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(map[string]any{ - "schedule": tc.schedule, - }) - - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/%s/rules/%s/schedule", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("UpdateRuleSchedule", mock.Anything, mock.Anything, mock.Anything).Return(tc.svcResp, tc.svcErr) - - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestEnableRuleEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp re.Rule - svcErr error - status int - authnErr error - err error - }{ - { - desc: "enable rule successfully", - token: validToken, - domainID: domainID, - id: validID, - svcResp: rule, - svcErr: nil, - status: http.StatusOK, - err: nil, - }, - { - desc: "enable rule with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "enable rule with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "enable rule with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "enable rule with service error", - token: validToken, - id: validID, - domainID: domainID, - svcResp: re.Rule{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "enable rule with empty id", - token: validToken, - id: "", - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/rules/%s/enable", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("EnableRule", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestDisableRuleEndpoint(t *testing.T) { - gs, svc, authn := newRuleEngineServer() - defer gs.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp re.Rule - svcErr error - status int - authnErr error - err error - }{ - { - desc: "disable rule successfully", - token: validToken, - domainID: domainID, - id: validID, - svcResp: rule, - svcErr: nil, - status: http.StatusOK, - err: nil, - }, - { - desc: "disable rule with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "disable rule with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "disable rule with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "disable rule with service error", - token: validToken, - id: validID, - domainID: domainID, - svcResp: re.Rule{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "disable rule with empty id", - token: validToken, - id: "", - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: gs.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/rules/%s/disable", gs.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("DisableRule", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestDeleteRuleEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcErr error - status int - authnErr error - err error - }{ - { - desc: "delete rule successfully", - token: validToken, - domainID: domainID, - id: validID, - svcErr: nil, - status: http.StatusNoContent, - err: nil, - }, - { - desc: "delete rule with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "delete rule with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "delete rule with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "delete rule with service error", - token: validToken, - id: validID, - domainID: domainID, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodDelete, - url: fmt.Sprintf("%s/%s/rules/%s", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("RemoveRule", mock.Anything, tc.session, tc.id).Return(tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -type respBody struct { - Err string `json:"error"` - Message string `json:"message"` - Total uint64 `json:"total"` - ID string `json:"id"` - Status re.Status `json:"status"` -} diff --git a/re/api/requests.go b/re/api/requests.go deleted file mode 100644 index 8aa9547c2..000000000 --- a/re/api/requests.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/re" -) - -const ( - maxLimitSize = 1000 - MaxNameSize = 1024 - MaxTitleSize = 37 -) - -type addRuleReq struct { - re.Rule -} - -func (req addRuleReq) validate() error { - if len(req.Name) > api.MaxNameSize || req.Name == "" { - return apiutil.ErrNameSize - } - if err := req.Rule.Schedule.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - - return nil -} - -type viewRuleReq struct { - id string - withRoles bool -} - -func (req viewRuleReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type listRulesReq struct { - re.PageMeta -} - -func (req listRulesReq) validate() error { - if req.Limit > maxLimitSize { - return apiutil.ErrLimitSize - } - - switch req.Order { - case "", api.NameKey, api.CreatedAtOrder, api.UpdatedAtOrder: - default: - return errors.Wrap(apiutil.ErrInvalidOrder, apiutil.ErrValidation) - } - - if req.Dir != api.AscDir && req.Dir != api.DescDir { - return apiutil.ErrInvalidDirection - } - - return nil -} - -type updateRuleReq struct { - Rule re.Rule -} - -func (req updateRuleReq) validate() error { - if req.Rule.ID == "" { - return apiutil.ErrMissingID - } - if len(req.Rule.Name) > api.MaxNameSize { - return apiutil.ErrNameSize - } - - return nil -} - -type updateRuleTagsReq struct { - id string - Tags []string `json:"tags,omitempty"` -} - -func (req updateRuleTagsReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type updateRuleScheduleReq struct { - id string - Schedule schedule.Schedule `json:"schedule,omitempty"` -} - -func (req updateRuleScheduleReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - if err := req.Schedule.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - - return nil -} - -type updateRuleStatusReq struct { - id string -} - -func (req updateRuleStatusReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type deleteRuleReq struct { - id string -} - -func (req deleteRuleReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} diff --git a/re/api/responses.go b/re/api/responses.go deleted file mode 100644 index 9c71033b7..000000000 --- a/re/api/responses.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "fmt" - "net/http" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/re" -) - -var ( - _ magistrala.Response = (*viewRuleRes)(nil) - _ magistrala.Response = (*addRuleRes)(nil) - _ magistrala.Response = (*updateRuleStatusRes)(nil) - _ magistrala.Response = (*rulesPageRes)(nil) - _ magistrala.Response = (*updateRuleRes)(nil) - _ magistrala.Response = (*deleteRuleRes)(nil) -) - -type pageRes struct { - Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset"` - Total uint64 `json:"total"` -} - -type addRuleRes struct { - re.Rule - created bool -} - -func (res addRuleRes) Code() int { - if res.created { - return http.StatusCreated - } - - return http.StatusOK -} - -func (res addRuleRes) Headers() map[string]string { - if res.created { - return map[string]string{ - "Location": fmt.Sprintf("/rules/%s", res.ID), - } - } - - return map[string]string{} -} - -func (res addRuleRes) Empty() bool { - return false -} - -type updateRuleRes struct { - re.Rule `json:",inline"` -} - -func (res updateRuleRes) Code() int { - return http.StatusOK -} - -func (res updateRuleRes) Headers() map[string]string { - return map[string]string{} -} - -func (res updateRuleRes) Empty() bool { - return false -} - -type viewRuleRes struct { - re.Rule `json:",inline"` -} - -func (res viewRuleRes) Code() int { - return http.StatusOK -} - -func (res viewRuleRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewRuleRes) Empty() bool { - return false -} - -type rulesPageRes struct { - re.Page `json:",inline"` -} - -func (res rulesPageRes) Code() int { - return http.StatusOK -} - -func (res rulesPageRes) Headers() map[string]string { - return map[string]string{} -} - -func (res rulesPageRes) Empty() bool { - return false -} - -type updateRuleStatusRes struct { - re.Rule `json:",inline"` -} - -func (res updateRuleStatusRes) Code() int { - return http.StatusOK -} - -func (res updateRuleStatusRes) Headers() map[string]string { - return map[string]string{} -} - -func (res updateRuleStatusRes) Empty() bool { - return false -} - -type deleteRuleRes struct { - deleted bool -} - -func (res deleteRuleRes) Code() int { - if res.deleted { - return http.StatusNoContent - } - - return http.StatusOK -} - -func (res deleteRuleRes) Headers() map[string]string { - return map[string]string{} -} - -func (res deleteRuleRes) Empty() bool { - return true -} diff --git a/re/api/transport.go b/re/api/transport.go deleted file mode 100644 index 44d09bbbb..000000000 --- a/re/api/transport.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "encoding/json" - "log/slog" - "net/http" - "strings" - - "github.com/absmach/magistrala" - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/re" - "github.com/go-chi/chi/v5" - kithttp "github.com/go-kit/kit/transport/http" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -const ( - ruleIdKey = "ruleID" - inputChannelKey = "input_channel" -) - -// MakeHandler creates an HTTP handler for the service endpoints. -func MakeHandler(svc re.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - mux.Group(func(r chi.Router) { - r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()) - r.Route("/{domainID}", func(r chi.Router) { - r.Route("/rules", func(r chi.Router) { - r.Post("/", otelhttp.NewHandler(kithttp.NewServer( - addRuleEndpoint(svc), - decodeAddRuleRequest, - api.EncodeResponse, - opts..., - ), "create_rule").ServeHTTP) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - listRulesEndpoint(svc), - decodeListRulesRequest, - api.EncodeResponse, - opts..., - ), "list_rules").ServeHTTP) - - r.Route("/{ruleID}", func(r chi.Router) { - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - viewRuleEndpoint(svc), - decodeViewRuleRequest, - api.EncodeResponse, - opts..., - ), "view_rule").ServeHTTP) - - r.Patch("/", otelhttp.NewHandler(kithttp.NewServer( - updateRuleEndpoint(svc), - decodeUpdateRuleRequest, - api.EncodeResponse, - opts..., - ), "update_rule").ServeHTTP) - - r.Patch("/tags", otelhttp.NewHandler(kithttp.NewServer( - updateRuleTagsEndpoint(svc), - decodeUpdateRuleTags, - api.EncodeResponse, - opts..., - ), "update_rule_tags").ServeHTTP) - - r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer( - updateRuleScheduleEndpoint(svc), - decodeUpdateRuleScheduleRequest, - api.EncodeResponse, - opts..., - ), "update_rule_scheduler").ServeHTTP) - - r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( - deleteRuleEndpoint(svc), - decodeDeleteRuleRequest, - api.EncodeResponse, - opts..., - ), "delete_rule").ServeHTTP) - - r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer( - enableRuleEndpoint(svc), - decodeUpdateRuleStatusRequest, - api.EncodeResponse, - opts..., - ), "enable_rule").ServeHTTP) - - r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer( - disableRuleEndpoint(svc), - decodeUpdateRuleStatusRequest, - api.EncodeResponse, - opts..., - ), "disable_rule").ServeHTTP) - }) - }) - }) - }) - - mux.Get("/health", magistrala.Health("rule_engine", instanceID)) - mux.Handle("/metrics", promhttp.Handler()) - - return mux -} - -func decodeAddRuleRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - var rule re.Rule - if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { - return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - return addRuleReq{Rule: rule}, nil -} - -func decodeViewRuleRequest(_ context.Context, r *http.Request) (any, error) { - id := chi.URLParam(r, ruleIdKey) - withRoles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - return viewRuleReq{id: id, withRoles: withRoles}, nil -} - -func decodeUpdateRuleRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - var rule re.Rule - if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { - return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - rule.ID = chi.URLParam(r, ruleIdKey) - - return updateRuleReq{Rule: rule}, nil -} - -func decodeUpdateRuleTags(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - - req := updateRuleTagsReq{ - id: chi.URLParam(r, ruleIdKey), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - - return req, nil -} - -func decodeUpdateRuleScheduleRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - - req := updateRuleScheduleReq{ - id: chi.URLParam(r, ruleIdKey), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - - return req, nil -} - -func decodeUpdateRuleStatusRequest(_ context.Context, r *http.Request) (any, error) { - req := updateRuleStatusReq{ - id: chi.URLParam(r, ruleIdKey), - } - - return req, nil -} - -func decodeListRulesRequest(_ context.Context, r *http.Request) (any, error) { - offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - name, err := apiutil.ReadStringQuery(r, api.NameKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - ic, err := apiutil.ReadStringQuery(r, inputChannelKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefStatus) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - dir, err := apiutil.ReadStringQuery(r, api.DirKey, "desc") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - st, err := re.ToStatus(s) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - tag, err := apiutil.ReadStringQuery(r, api.TagKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - return listRulesReq{ - PageMeta: re.PageMeta{ - Offset: offset, - Limit: limit, - Name: name, - InputChannel: ic, - Status: st, - Dir: dir, - Order: order, - Tag: tag, - }, - }, nil -} - -func decodeDeleteRuleRequest(_ context.Context, r *http.Request) (any, error) { - id := chi.URLParam(r, ruleIdKey) - - return deleteRuleReq{id: id}, nil -} diff --git a/re/atom.go b/re/atom.go deleted file mode 100644 index 2bba99dc1..000000000 --- a/re/atom.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" -) - -type atomService struct { - Service - projector atom.Projector -} - -func WithAtom(svc Service, projector atom.Projector) Service { - if projector == nil { - return svc - } - return atomService{Service: svc, projector: projector} -} - -func (svc atomService) AddRule(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - rule, err := svc.Service.AddRule(ctx, session, r) - if err != nil { - return rule, err - } - if err := svc.projector.UpsertResource(ctx, ruleProjection(rule)); err != nil { - return rule, nil - } - return rule, nil -} - -func (svc atomService) UpdateRule(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - rule, err := svc.Service.UpdateRule(ctx, session, r) - return svc.upsertAfterRuleChange(ctx, rule, err) -} - -func (svc atomService) UpdateRuleTags(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - rule, err := svc.Service.UpdateRuleTags(ctx, session, r) - return svc.upsertAfterRuleChange(ctx, rule, err) -} - -func (svc atomService) UpdateRuleSchedule(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - rule, err := svc.Service.UpdateRuleSchedule(ctx, session, r) - return svc.upsertAfterRuleChange(ctx, rule, err) -} - -func (svc atomService) EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error) { - rule, err := svc.Service.EnableRule(ctx, session, id) - return svc.upsertAfterRuleChange(ctx, rule, err) -} - -func (svc atomService) DisableRule(ctx context.Context, session authn.Session, id string) (Rule, error) { - rule, err := svc.Service.DisableRule(ctx, session, id) - return svc.upsertAfterRuleChange(ctx, rule, err) -} - -func (svc atomService) RemoveRule(ctx context.Context, session authn.Session, id string) error { - if err := svc.Service.RemoveRule(ctx, session, id); err != nil { - return err - } - _ = svc.projector.DeleteResource(ctx, id) - return nil -} - -func (svc atomService) upsertAfterRuleChange(ctx context.Context, rule Rule, err error) (Rule, error) { - if err != nil { - return rule, err - } - if err := svc.projector.UpsertResource(ctx, ruleProjection(rule)); err != nil { - return rule, nil - } - return rule, nil -} - -func ruleProjection(r Rule) atom.Resource { - res := atom.ResourceFromFields(atom.ObjectFields{ - ID: r.ID, - Kind: atom.KindRule, - Name: r.Name, - TenantID: r.DomainID, - OwnerID: r.CreatedBy, - Status: r.Status.String(), - Tags: r.Tags, - CreatedBy: r.CreatedBy, - UpdatedBy: r.UpdatedBy, - CreatedAt: r.CreatedAt, - UpdatedAt: r.UpdatedAt, - }) - res.Attributes["input_channel"] = r.InputChannel - res.Attributes["input_topic"] = r.InputTopic - return res -} diff --git a/re/atom_test.go b/re/atom_test.go deleted file mode 100644 index 73961cfc3..000000000 --- a/re/atom_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "testing" - "time" - - "github.com/absmach/magistrala/pkg/schedule" -) - -func TestRuleProjectionOmitsMetadataAndSchedule(t *testing.T) { - got := ruleProjection(Rule{ - ID: "rule-1", - Name: "high-temp", - DomainID: "domain-1", - CreatedBy: "user-1", - Status: EnabledStatus, - Tags: []string{"smoke"}, - Metadata: Metadata{"flow": "encoded-flow", "other": "value"}, - InputChannel: "channel-1", - InputTopic: "messages", - Schedule: schedule.Schedule{ - Time: time.Date(2026, 6, 26, 17, 0, 0, 0, time.UTC), - }, - }) - - if _, ok := got.Attributes["metadata"]; ok { - t.Fatalf("rule metadata should not be projected to Atom attributes: %+v", got.Attributes) - } - if _, ok := got.Attributes["scheduled_at"]; ok { - t.Fatalf("rule schedule should not be projected to Atom attributes: %+v", got.Attributes) - } - if got.Attributes["input_channel"] != "channel-1" { - t.Fatalf("unexpected input_channel: %+v", got.Attributes) - } - if got.Attributes["input_topic"] != "messages" { - t.Fatalf("unexpected input_topic: %+v", got.Attributes) - } -} diff --git a/re/doc.go b/re/doc.go deleted file mode 100644 index 2c28de3bd..000000000 --- a/re/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package re contain the domain concept definitions needed to -// support Magistrala Rule Egine services functionality. -package re diff --git a/re/events/doc.go b/re/events/doc.go deleted file mode 100644 index 720686489..000000000 --- a/re/events/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package events provides the domain concept definitions needed to support -// clients events functionality. -package events diff --git a/re/events/events.go b/re/events/events.go deleted file mode 100644 index 7e807c286..000000000 --- a/re/events/events.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "maps" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/re" -) - -const ( - rulePrefix = "rule." - ruleCreate = rulePrefix + "create" - ruleList = rulePrefix + "list" - ruleView = rulePrefix + "view" - ruleUpdate = rulePrefix + "update" - ruleUpdateTags = rulePrefix + "update_tags" - ruleUpdateSchedule = rulePrefix + "update_schedule" - ruleEnable = rulePrefix + "enable" - ruleDisable = rulePrefix + "disable" - ruleRemove = rulePrefix + "remove" -) - -var ( - _ events.Event = (*createRuleEvent)(nil) - _ events.Event = (*listRuleEvent)(nil) - _ events.Event = (*viewRuleEvent)(nil) - _ events.Event = (*updateRuleEvent)(nil) - _ events.Event = (*updateRuleTagsEvent)(nil) - _ events.Event = (*updateRuleScheduleEvent)(nil) - _ events.Event = (*enableRuleEvent)(nil) - _ events.Event = (*disableRuleEvent)(nil) - _ events.Event = (*removeRuleEvent)(nil) -) - -type baseRuleEvent struct { - session authn.Session - requestID string -} - -func newBaseRuleEvent(session authn.Session, requestID string) baseRuleEvent { - return baseRuleEvent{ - session: session, - requestID: requestID, - } -} - -func (bre baseRuleEvent) Encode() map[string]any { - return map[string]any{ - "domain": bre.session.DomainID, - "user_id": bre.session.UserID, - "token_type": bre.session.Type.String(), - "super_admin": bre.session.SuperAdmin, - "request_id": bre.requestID, - } -} - -type createRuleEvent struct { - rule re.Rule - baseRuleEvent -} - -func (cre createRuleEvent) Encode() (map[string]any, error) { - val, err := cre.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, cre.baseRuleEvent.Encode()) - val["operation"] = ruleCreate - return val, nil -} - -type listRuleEvent struct { - re.PageMeta - baseRuleEvent -} - -// Encode implements the events.Event interface for listRuleEvent. -func (lre listRuleEvent) Encode() (map[string]any, error) { - val := lre.EventEncode() - maps.Copy(val, lre.baseRuleEvent.Encode()) - val["operation"] = ruleList - return val, nil -} - -type updateRuleEvent struct { - rule re.Rule - baseRuleEvent -} - -type viewRuleEvent struct { - rule re.Rule - baseRuleEvent -} - -func (vre viewRuleEvent) Encode() (map[string]any, error) { - val, err := vre.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, vre.baseRuleEvent.Encode()) - val["operation"] = ruleView - return val, nil -} - -func (ure updateRuleEvent) Encode() (map[string]any, error) { - val, err := ure.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, ure.baseRuleEvent.Encode()) - val["operation"] = ruleUpdate - return val, nil -} - -type updateRuleTagsEvent struct { - rule re.Rule - baseRuleEvent -} - -func (urte updateRuleTagsEvent) Encode() (map[string]any, error) { - val, err := urte.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, urte.baseRuleEvent.Encode()) - val["operation"] = ruleUpdateTags - return val, nil -} - -type updateRuleScheduleEvent struct { - rule re.Rule - baseRuleEvent -} - -func (urse updateRuleScheduleEvent) Encode() (map[string]any, error) { - val, err := urse.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, urse.baseRuleEvent.Encode()) - val["operation"] = ruleUpdateSchedule - return val, nil -} - -type disableRuleEvent struct { - rule re.Rule - baseRuleEvent -} - -func (dre disableRuleEvent) Encode() (map[string]any, error) { - val, err := dre.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, dre.baseRuleEvent.Encode()) - val["operation"] = ruleDisable - return val, nil -} - -type enableRuleEvent struct { - rule re.Rule - baseRuleEvent -} - -func (ere enableRuleEvent) Encode() (map[string]any, error) { - val, err := ere.rule.EventEncode() - if err != nil { - return map[string]any{}, err - } - maps.Copy(val, ere.baseRuleEvent.Encode()) - val["operation"] = ruleEnable - return val, nil -} - -type removeRuleEvent struct { - id string - baseRuleEvent -} - -func (rre removeRuleEvent) Encode() (map[string]any, error) { - val := rre.baseRuleEvent.Encode() - val["id"] = rre.id - val["operation"] = ruleRemove - return val, nil -} diff --git a/re/events/streams.go b/re/events/streams.go deleted file mode 100644 index cff166702..000000000 --- a/re/events/streams.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/events/store" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re" - "github.com/go-chi/chi/v5/middleware" -) - -const ( - magistralaPrefix = "magistrala." - CreateStream = magistralaPrefix + ruleCreate - ListStream = magistralaPrefix + ruleList - ViewStream = magistralaPrefix + ruleView - UpdateStream = magistralaPrefix + ruleUpdate - UpdateTagsStream = magistralaPrefix + ruleUpdateTags - UpdateScheduleStream = magistralaPrefix + ruleUpdateSchedule - EnableStream = magistralaPrefix + ruleEnable - DisableStream = magistralaPrefix + ruleDisable - RemoveStream = magistralaPrefix + ruleRemove -) - -var _ re.Service = (*eventStore)(nil) - -type eventStore struct { - events.Publisher - svc re.Service -} - -// NewEventStoreMiddleware returns wrapper around rules service that sends -// events to event store. -func NewEventStoreMiddleware(ctx context.Context, svc re.Service, url string) (re.Service, error) { - publisher, err := store.NewPublisher(ctx, url, "re-es-pub") - if err != nil { - return nil, err - } - - return &eventStore{ - svc: svc, - Publisher: publisher, - }, nil -} - -func (es *eventStore) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - rule, err := es.svc.AddRule(ctx, session, r) - if err != nil { - return rule, err - } - event := createRuleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, CreateStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - page, err := es.svc.ListRules(ctx, session, pm) - if err != nil { - return page, err - } - event := listRuleEvent{ - PageMeta: pm, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, ListStream, event); err != nil { - return page, err - } - return page, nil -} - -func (es *eventStore) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - rule, err := es.svc.ViewRule(ctx, session, id, withRoles) - if err != nil { - return rule, err - } - event := viewRuleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, ViewStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - rule, err := es.svc.UpdateRule(ctx, session, r) - if err != nil { - return rule, err - } - event := updateRuleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, UpdateStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - rule, err := es.svc.UpdateRuleTags(ctx, session, r) - if err != nil { - return rule, err - } - event := updateRuleTagsEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, UpdateTagsStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - rule, err := es.svc.UpdateRuleSchedule(ctx, session, r) - if err != nil { - return rule, err - } - event := updateRuleScheduleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, UpdateScheduleStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) RemoveRule(ctx context.Context, session authn.Session, id string) error { - err := es.svc.RemoveRule(ctx, session, id) - if err != nil { - return err - } - event := removeRuleEvent{ - id: id, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, RemoveStream, event); err != nil { - return err - } - return nil -} - -func (es *eventStore) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - rule, err := es.svc.EnableRule(ctx, session, id) - if err != nil { - return rule, err - } - event := enableRuleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, EnableStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - rule, err := es.svc.DisableRule(ctx, session, id) - if err != nil { - return rule, err - } - event := disableRuleEvent{ - rule: rule, - baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, DisableStream, event); err != nil { - return rule, err - } - return rule, nil -} - -func (es *eventStore) StartScheduler(ctx context.Context) error { - return es.svc.StartScheduler(ctx) -} - -func (es *eventStore) Handle(msg *messaging.Message) error { - return es.svc.Handle(msg) -} - -func (es *eventStore) Cancel() error { - return es.svc.Cancel() -} diff --git a/re/events/streams_test.go b/re/events/streams_test.go deleted file mode 100644 index 543f2e5c1..000000000 --- a/re/events/streams_test.go +++ /dev/null @@ -1,576 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/events" - "github.com/absmach/magistrala/re/mocks" - "github.com/go-chi/chi/v5/middleware" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - storeClient *redis.Client - storeURL string - validSession = authn.Session{ - DomainID: testsutil.GenerateUUID(&testing.T{}), - UserID: testsutil.GenerateUUID(&testing.T{}), - } - validRule = generateTestRule(&testing.T{}) - validPage = re.Page{ - Offset: 0, - Limit: 10, - Total: 1, - Rules: []re.Rule{validRule}, - } -) - -func newEventStoreMiddleware(t *testing.T) (*mocks.Service, re.Service) { - svc := new(mocks.Service) - nsvc, err := events.NewEventStoreMiddleware(context.Background(), svc, storeURL) - require.Nil(t, err, fmt.Sprintf("create events store middleware failed with unexpected error: %s", err)) - - return svc, nsvc -} - -func TestMain(m *testing.M) { - code := testsutil.RunRedisTest(m, &storeClient, &storeURL) - os.Exit(code) -} - -func TestAddRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - rule re.Rule - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - rule: validRule, - svcRes: validRule, - svcErr: nil, - resp: validRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - rule: validRule, - svcRes: re.Rule{}, - svcErr: svcerr.ErrCreateEntity, - resp: re.Rule{}, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("AddRule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.AddRule(validCtx, tc.session, tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestViewRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - ruleID string - withRoles bool - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - ruleID: validRule.ID, - withRoles: false, - svcRes: validRule, - svcErr: nil, - resp: validRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - ruleID: validRule.ID, - withRoles: false, - svcRes: re.Rule{}, - svcErr: svcerr.ErrViewEntity, - resp: re.Rule{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("ViewRule", validCtx, tc.session, tc.ruleID, tc.withRoles).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.ViewRule(validCtx, tc.session, tc.ruleID, tc.withRoles) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - updatedRule := validRule - updatedRule.Name = "updatedName" - - cases := []struct { - desc string - session authn.Session - rule re.Rule - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - rule: updatedRule, - svcRes: updatedRule, - svcErr: nil, - resp: updatedRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - rule: updatedRule, - svcRes: re.Rule{}, - svcErr: svcerr.ErrUpdateEntity, - resp: re.Rule{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateRule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.UpdateRule(validCtx, tc.session, tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateRuleTags(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - taggedRule := validRule - taggedRule.Tags = []string{"newtag1", "newtag2"} - - cases := []struct { - desc string - session authn.Session - rule re.Rule - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - rule: taggedRule, - svcRes: taggedRule, - svcErr: nil, - resp: taggedRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - rule: taggedRule, - svcRes: re.Rule{}, - svcErr: svcerr.ErrUpdateEntity, - resp: re.Rule{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateRuleTags", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.UpdateRuleTags(validCtx, tc.session, tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateRuleSchedule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - rule re.Rule - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - rule: validRule, - svcRes: validRule, - svcErr: nil, - resp: validRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - rule: validRule, - svcRes: re.Rule{}, - svcErr: svcerr.ErrUpdateEntity, - resp: re.Rule{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateRuleSchedule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.UpdateRuleSchedule(validCtx, tc.session, tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestListRules(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - pageMeta re.PageMeta - svcRes re.Page - svcErr error - resp re.Page - err error - }{ - { - desc: "publish successfully", - session: validSession, - pageMeta: re.PageMeta{ - Limit: 10, - Offset: 0, - }, - svcRes: validPage, - svcErr: nil, - resp: validPage, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - pageMeta: re.PageMeta{ - Limit: 10, - Offset: 0, - }, - svcRes: re.Page{}, - svcErr: svcerr.ErrViewEntity, - resp: re.Page{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("ListRules", validCtx, tc.session, tc.pageMeta).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.ListRules(validCtx, tc.session, tc.pageMeta) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestRemoveRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - ruleID string - svcErr error - err error - }{ - { - desc: "publish successfully", - session: validSession, - ruleID: validRule.ID, - svcErr: nil, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - ruleID: validRule.ID, - svcErr: svcerr.ErrRemoveEntity, - err: svcerr.ErrRemoveEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("RemoveRule", validCtx, tc.session, tc.ruleID).Return(tc.svcErr) - err := nsvc.RemoveRule(validCtx, tc.session, tc.ruleID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestEnableRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - ruleID string - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - ruleID: validRule.ID, - svcRes: validRule, - svcErr: nil, - resp: validRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - ruleID: validRule.ID, - svcRes: re.Rule{}, - svcErr: svcerr.ErrUpdateEntity, - resp: re.Rule{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("EnableRule", validCtx, tc.session, tc.ruleID).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.EnableRule(validCtx, tc.session, tc.ruleID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestDisableRule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - ruleID string - svcRes re.Rule - svcErr error - resp re.Rule - err error - }{ - { - desc: "publish successfully", - session: validSession, - ruleID: validRule.ID, - svcRes: validRule, - svcErr: nil, - resp: validRule, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - ruleID: validRule.ID, - svcRes: re.Rule{}, - svcErr: svcerr.ErrUpdateEntity, - resp: re.Rule{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("DisableRule", validCtx, tc.session, tc.ruleID).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.DisableRule(validCtx, tc.session, tc.ruleID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestStartScheduler(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - cases := []struct { - desc string - svcErr error - err error - }{ - { - desc: "start scheduler successfully", - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - svcErr: svcerr.ErrCreateEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("StartScheduler", context.Background()).Return(tc.svcErr) - err := nsvc.StartScheduler(context.Background()) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestHandle(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - msg := &messaging.Message{Channel: "test.channel"} - - cases := []struct { - desc string - msg *messaging.Message - svcErr error - err error - }{ - { - desc: "handle successfully", - msg: msg, - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - msg: msg, - svcErr: svcerr.ErrCreateEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("Handle", tc.msg).Return(tc.svcErr) - err := nsvc.Handle(tc.msg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestCancel(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - cases := []struct { - desc string - svcErr error - err error - }{ - { - desc: "cancel successfully", - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - svcErr: svcerr.ErrCreateEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("Cancel").Return(tc.svcErr) - err := nsvc.Cancel() - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func generateTestRule(t *testing.T) re.Rule { - createdAt, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") - assert.Nil(t, err, fmt.Sprintf("Unexpected error parsing time: %v", err)) - return re.Rule{ - ID: testsutil.GenerateUUID(t), - Name: "testrule", - DomainID: testsutil.GenerateUUID(t), - InputChannel: "test.channel", - Status: re.EnabledStatus, - CreatedAt: createdAt, - UpdatedAt: createdAt, - } -} diff --git a/re/golang.go b/re/golang.go deleted file mode 100644 index 0cc42e02b..000000000 --- a/re/golang.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "reflect" - "regexp" - - "github.com/absmach/magistrala/pkg/errors" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - golang "github.com/traefik/yaegi/interp" - "github.com/traefik/yaegi/stdlib" -) - -const logicFunction = "main.logicFunction" - -var ( - goKeywordRegex = regexp.MustCompile(`\bgo\s+func\s*\(|^\s*go\s+\w+\(|[;\s{]go\s+func\s*\(|[;\s{]go\s+\w+\(`) - panicRegex = regexp.MustCompile(`\bpanic\s*\(`) -) - -// Type message is a magistrala message with payload replaces by JSON deserialized payload. -type message struct { - Channel string `json:"channel,omitempty"` - ClientID string `json:"client_id,omitempty"` - Domain string `json:"domain,omitempty"` - Subtopic string `json:"subtopic,omitempty"` - Publisher string `json:"publisher,omitempty"` - Protocol string `json:"protocol,omitempty"` - Created int64 `json:"created,omitempty"` - Payload any `json:"payload,omitempty"` -} - -func (re *re) processGo(ctx context.Context, details []slog.Attr, r Rule, msg *messaging.Message) (ret pkglog.RunInfo) { - defer func() { - if r := recover(); r != nil { - ret = pkglog.RunInfo{ - Level: slog.LevelError, - Details: details, - Message: fmt.Sprintf("panic in Go script: %v", r), - } - } - }() - - i := golang.New(golang.Options{}) - if err := i.Use(stdlib.Symbols); err != nil { - return pkglog.RunInfo{Level: slog.LevelError, Details: details, Message: err.Error()} - } - m := message{ - Created: msg.Created, - ClientID: msg.ClientIdentity(), - Domain: msg.Domain, - Publisher: msg.Publisher, - Channel: msg.Channel, - Subtopic: msg.Subtopic, - Protocol: msg.Protocol, - } - var pld any - if err := json.Unmarshal(msg.Payload, &pld); err != nil { - pld = msg.Payload - } - m.Payload = pld - - err := i.Use(golang.Exports{ - "messaging/m": { - "message": reflect.ValueOf(m), - }, - }) - if err != nil { - return pkglog.RunInfo{Level: slog.LevelError, Details: details, Message: err.Error()} - } - if _, err = i.Eval(r.Logic.Value); err != nil { - return pkglog.RunInfo{Level: slog.LevelError, Details: details, Message: err.Error()} - } - ifc, err := i.Eval(logicFunction) - if err != nil { - return pkglog.RunInfo{Level: slog.LevelError, Details: details, Message: err.Error()} - } - f, ok := ifc.Interface().(func() any) - if !ok { - return pkglog.RunInfo{Level: slog.LevelError, Message: "invalid logic function signature", Details: details} - } - res := f() - if b, ok := res.(bool); ok && !b { - return pkglog.RunInfo{Level: slog.LevelInfo, Message: "logic returned false", Details: details} - } - for _, o := range r.Outputs { - if e := re.handleOutput(ctx, o, r, msg, res); e != nil { - err = errors.Wrap(e, err) - } - } - ret = pkglog.RunInfo{Level: slog.LevelInfo, Details: details, Message: "rule processed successfully"} - if err != nil { - ret.Level = slog.LevelError - ret.Message = fmt.Sprintf("failed to handle rule output: %s", err) - } - return ret -} diff --git a/re/handlers.go b/re/handlers.go deleted file mode 100644 index d042c2deb..000000000 --- a/re/handlers.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "strings" - "time" - - "github.com/absmach/magistrala/pkg/errors" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re/outputs" -) - -var ( - scheduledTrue = true - scheduledFalse = false -) - -const ( - maxPayload = 100 * 1024 - pldExceededFmt = "max payload size of 100kB exceeded: " - protocol = "nats" -) - -func (re *re) Handle(msg *messaging.Message) error { - // Limit payload for RE so we don't get to process large JSON. - if n := len(msg.Payload); n > maxPayload { - return errors.New(pldExceededFmt + strconv.Itoa(n)) - } - // Skip filtering by message topic and fetch all non-scheduled rules instead. - // It's cleaner and more efficient to match wildcards in Go, but we can - // revisit this if it ever becomes a performance bottleneck. - pm := PageMeta{ - Domain: msg.Domain, - InputChannel: msg.Channel, - Status: EnabledStatus, - Scheduled: &scheduledFalse, - } - ctx := context.Background() - page, err := re.repo.ListAllRules(ctx, pm) - if err != nil { - return err - } - for _, r := range page.Rules { - if matchTopic(msg.Subtopic, r.InputTopic) { - go func(ctx context.Context) { - re.runInfo <- re.process(ctx, r, msg) - }(ctx) - } - } - - return nil -} - -// matchTopic matches a published subtopic against a subscription pattern -// using MQTT-style wildcards: + (single level) and # (multi-level). -func matchTopic(published, subscribed string) bool { - p := strings.Split(published, "/") - s := strings.Split(subscribed, "/") - n := len(p) - - for i := range s { - if s[i] == "#" { - return true - } - if i >= n { - return false - } - if s[i] != "+" && p[i] != s[i] { - return false - } - } - return len(s) == n -} - -func (re *re) process(ctx context.Context, r Rule, msg *messaging.Message) pkglog.RunInfo { - details := []slog.Attr{ - slog.String("domain_id", r.DomainID), - slog.String("rule_id", r.ID), - slog.String("rule_name", r.Name), - slog.Time("exec_time", time.Now().UTC()), - } - switch r.Logic.Type { - case GoType: - return re.processGo(ctx, details, r, msg) - default: - return re.processLua(ctx, details, r, msg) - } -} - -func (re *re) handleOutput(ctx context.Context, o Runnable, r Rule, msg *messaging.Message, val any) error { - switch o := o.(type) { - case *outputs.Alarm: - o.AlarmsPub = re.alarmsPub - o.RuleID = r.ID - return o.Run(ctx, msg, val) - case *outputs.Email: - o.Emailer = re.email - return o.Run(ctx, msg, val) - case *outputs.ChannelPublisher: - o.RePubSub = re.rePubSub - return o.Run(ctx, msg, val) - case *outputs.SenML: - o.WritersPub = re.writersPub - return o.Run(ctx, msg, val) - case *outputs.Postgres, *outputs.Slack: - return o.Run(ctx, msg, val) - default: - return fmt.Errorf("unknown output type: %T", o) - } -} - -func (re *re) StartScheduler(ctx context.Context) error { - defer re.ticker.Stop() - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-re.ticker.Tick(): - due := time.Now().UTC() - pm := PageMeta{ - Status: EnabledStatus, - Scheduled: &scheduledTrue, - ScheduledBefore: &due, - } - - page, err := re.repo.ListAllRules(ctx, pm) - if err != nil { - re.runInfo <- pkglog.RunInfo{ - Level: slog.LevelError, - Message: fmt.Sprintf("failed to list rules: %s", err), - Details: []slog.Attr{slog.Time("due", due)}, - } - - continue - } - - for _, r := range page.Rules { - go func(rule Rule, dueTime time.Time) { - if _, err := re.repo.UpdateRuleDue(ctx, rule.ID, rule.Schedule.NextDue()); err != nil { - re.runInfo <- pkglog.RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to update rule: %s", err), Details: []slog.Attr{slog.Time("time", time.Now().UTC())}} - return - } - - msg := &messaging.Message{ - Domain: rule.DomainID, - Channel: rule.InputChannel, - Subtopic: rule.InputTopic, - Protocol: protocol, - Created: dueTime.Unix(), - } - re.runInfo <- re.process(ctx, rule, msg) - }(r, due) - } - // Reset due, it will reset in the page meta as well. - due = time.Now().UTC() - } - } -} diff --git a/re/lua.go b/re/lua.go deleted file mode 100644 index dab540703..000000000 --- a/re/lua.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - - "github.com/absmach/magistrala/pkg/errors" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/vadv/gopher-lua-libs/argparse" - "github.com/vadv/gopher-lua-libs/base64" - bit "github.com/vadv/gopher-lua-libs/bit" - "github.com/vadv/gopher-lua-libs/crypto" - "github.com/vadv/gopher-lua-libs/db" - "github.com/vadv/gopher-lua-libs/filepath" - client "github.com/vadv/gopher-lua-libs/http/client" - "github.com/vadv/gopher-lua-libs/ioutil" - luajson "github.com/vadv/gopher-lua-libs/json" - "github.com/vadv/gopher-lua-libs/regexp" - "github.com/vadv/gopher-lua-libs/storage" - "github.com/vadv/gopher-lua-libs/strings" - luatime "github.com/vadv/gopher-lua-libs/time" - "github.com/vadv/gopher-lua-libs/yaml" - lua "github.com/yuin/gopher-lua" -) - -const payloadKey = "payload" - -func (re *re) processLua(ctx context.Context, details []slog.Attr, r Rule, msg *messaging.Message) pkglog.RunInfo { - l := lua.NewState() - defer l.Close() - preload(l) - message := prepareMsg(l, msg) - - // Set the message object as a Lua global variable. - l.SetGlobal("message", message) - if err := l.DoString(r.Logic.Value); err != nil { - return pkglog.RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to run rule logic: %s", err), Details: details} - } - // Get the last result. - result := l.Get(-1) - if result == lua.LNil { - return pkglog.RunInfo{Level: slog.LevelWarn, Message: "rule with nil script result", Details: details} - } - // Converting Lua is an expensive operation, so - // don't do it if there are no outputs. - if len(r.Outputs) == 0 { - return pkglog.RunInfo{Level: slog.LevelWarn, Message: "rule with no outputs", Details: details} - } - var err error - res := convertLua(result) - - for _, o := range r.Outputs { - // If value is false, don't run the follow-up. - if v, ok := res.(bool); ok && !v { - return pkglog.RunInfo{Level: slog.LevelInfo, Message: "logic returned false", Details: details} - } - if e := re.handleOutput(ctx, o, r, msg, res); e != nil { - err = errors.Wrap(e, err) - } - } - ret := pkglog.RunInfo{Level: slog.LevelInfo, Message: "rule processed successfully", Details: details} - if err != nil { - ret.Level = slog.LevelError - ret.Message = fmt.Sprintf("failed to handle rule output: %s", err) - } - return ret -} - -func preload(l *lua.LState) { - db.Preload(l) - ioutil.Preload(l) - luajson.Preload(l) - yaml.Preload(l) - crypto.Preload(l) - regexp.Preload(l) - luatime.Preload(l) - storage.Preload(l) - base64.Preload(l) - argparse.Preload(l) - strings.Preload(l) - filepath.Preload(l) - client.Preload(l) - bit.Preload(l) -} - -func prepareMsg(l *lua.LState, msg *messaging.Message) lua.LValue { - message := l.NewTable() - message.RawSetString("domain", lua.LString(msg.Domain)) - message.RawSetString("channel", lua.LString(msg.Channel)) - message.RawSetString("subtopic", lua.LString(msg.Subtopic)) - message.RawSetString("client_id", lua.LString(msg.ClientIdentity())) - message.RawSetString("publisher", lua.LString(msg.Publisher)) - message.RawSetString("protocol", lua.LString(msg.Protocol)) - message.RawSetString("created", lua.LNumber(msg.Created)) - - var payload any - if err := json.Unmarshal(msg.GetPayload(), &payload); err != nil { - pld := l.NewTable() - // If message is not JSON, set binary payload and exit. - for i, b := range msg.Payload { - // Lua tables are 1-indexed. - pld.Insert(i+1, lua.LNumber(b)) - } - message.RawSetString(payloadKey, pld) - return message - } - - // Payload is JSON, set the correct value. - message.RawSetString(payloadKey, traverseJson(l, payload)) - return message -} - -func traverseJson(l *lua.LState, value any) lua.LValue { - switch val := value.(type) { - case string: - return lua.LString(val) - case float64: - return lua.LNumber(val) - case int: - return lua.LNumber(float64(val)) - case json.Number: - if num, err := val.Float64(); err != nil { - return lua.LNumber(num) - } - return lua.LNil - case bool: - return lua.LBool(val) - case []any: - t := l.NewTable() - for i, j := range val { - t.RawSetInt(i+1, traverseJson(l, j)) - } - return t - case map[string]any: - t := l.NewTable() - for k, v := range val { - t.RawSetString(k, traverseJson(l, v)) - } - return t - default: - return lua.LNil - } -} - -func convertLua(lv lua.LValue) any { - switch v := lv.(type) { - case *lua.LTable: - isArray := true - v.ForEach(func(key, value lua.LValue) { - if key.Type() != lua.LTNumber { - isArray = false - } - }) - - if isArray { - arr := []any{} - v.ForEach(func(key, value lua.LValue) { - arr = append(arr, convertLua(value)) - }) - return arr - } - - obj := map[string]any{} - v.ForEach(func(key, value lua.LValue) { - obj[key.String()] = convertLua(value) - }) - return obj - case lua.LString: - return string(v) - case lua.LNumber: - return float64(v) - case lua.LBool: - return bool(v) - case *lua.LNilType: - return nil - default: - return v.String() - } -} diff --git a/re/middleware/authorization.go b/re/middleware/authorization.go deleted file mode 100644 index f34c9c1dd..000000000 --- a/re/middleware/authorization.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - smqauthz "github.com/absmach/magistrala/pkg/authz" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/policies" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/operations" -) - -var ( - errDomainCreateRules = errors.New("not authorized to create rules in domain") - errDomainViewRules = errors.New("not authorized to view rules in domain") - errDomainUpdateRules = errors.New("not authorized to update rules in domain") - errDomainDeleteRules = errors.New("not authorized to delete rules in domain") -) - -type authorizationMiddleware struct { - svc re.Service - authz smqauthz.Authorization - atomAuthz atom.Authorizer - entitiesOps permissions.EntitiesOperations[permissions.Operation] -} - -// AuthorizationMiddleware adds authorization to the re service. -func AuthorizationMiddleware(svc re.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (re.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - return &authorizationMiddleware{ - svc: svc, - authz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func AtomAuthorizationMiddleware(svc re.Service, authz atom.Authorizer, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (re.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - return &authorizationMiddleware{ - svc: svc, - atomAuthz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func (am *authorizationMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpAddRule, session, policies.DomainType, session.DomainID); err != nil { - return re.Rule{}, errors.Wrap(errDomainCreateRules, err) - } - - return am.svc.AddRule(ctx, session, r) -} - -func (am *authorizationMiddleware) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpViewRule, session, operations.EntityType, id); err != nil { - return re.Rule{}, errors.Wrap(errDomainViewRules, err) - } - - return am.svc.ViewRule(ctx, session, id, withRoles) -} - -func (am *authorizationMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpUpdateRule, session, operations.EntityType, r.ID); err != nil { - return re.Rule{}, errors.Wrap(errDomainUpdateRules, err) - } - - return am.svc.UpdateRule(ctx, session, r) -} - -func (am *authorizationMiddleware) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpUpdateRuleTags, session, operations.EntityType, r.ID); err != nil { - return re.Rule{}, errors.Wrap(errDomainUpdateRules, err) - } - - return am.svc.UpdateRuleTags(ctx, session, r) -} - -func (am *authorizationMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpUpdateRuleSchedule, session, operations.EntityType, r.ID); err != nil { - return re.Rule{}, errors.Wrap(errDomainUpdateRules, err) - } - - return am.svc.UpdateRuleSchedule(ctx, session, r) -} - -func (am *authorizationMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - switch err := am.checkSuperAdmin(ctx, session); { - case err == nil: - session.SuperAdmin = true - case errors.Contains(err, svcerr.ErrSuperAdminAction): - if err := am.authorize(ctx, operations.OpListRules, session, operations.EntityType, auth.AnyIDs); err != nil { - return re.Page{}, errors.Wrap(errDomainViewRules, err) - } - default: - return re.Page{}, err - } - - return am.svc.ListRules(ctx, session, pm) -} - -func (am *authorizationMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) error { - if err := am.authorize(ctx, operations.OpRemoveRule, session, operations.EntityType, id); err != nil { - return errors.Wrap(errDomainDeleteRules, err) - } - - return am.svc.RemoveRule(ctx, session, id) -} - -func (am *authorizationMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpEnableRule, session, operations.EntityType, id); err != nil { - return re.Rule{}, errors.Wrap(errDomainUpdateRules, err) - } - - return am.svc.EnableRule(ctx, session, id) -} - -func (am *authorizationMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - if err := am.authorize(ctx, operations.OpDisableRule, session, operations.EntityType, id); err != nil { - return re.Rule{}, errors.Wrap(errDomainUpdateRules, err) - } - - return am.svc.DisableRule(ctx, session, id) -} - -func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error { - return am.svc.StartScheduler(ctx) -} - -func (am *authorizationMiddleware) Handle(msg *messaging.Message) error { - return am.svc.Handle(msg) -} - -func (am *authorizationMiddleware) Cancel() error { - return am.svc.Cancel() -} - -func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error { - perm, err := am.entitiesOps.GetPermission(operations.EntityType, op) - if err != nil { - return err - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, perm.String(), objType, obj, atom.KindRule) - } - - pr := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: obj, - ObjectType: objType, - Permission: perm.String(), - } - - var pat *smqauthz.PATReq - if session.PatID != "" { - entityID := obj - if objType == policies.DomainType { - entityID = auth.AnyIDs - } - opName := am.entitiesOps.OperationName(operations.EntityType, op) - pat = &smqauthz.PATReq{ - UserID: session.UserID, - PatID: session.PatID, - EntityID: entityID, - EntityType: auth.RulesType.String(), - Operation: opName, - Domain: session.DomainID, - } - } - - if err := am.authz.Authorize(ctx, pr, pat); err != nil { - return err - } - - return nil -} - -func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { - if session.Role != authn.SuperAdminRole { - return svcerr.ErrSuperAdminAction - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, policies.AdminPermission, policies.PlatformType, policies.MagistralaObject, policies.PlatformType) - } - if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{ - SubjectType: policies.UserType, - Subject: session.UserID, - Permission: policies.AdminPermission, - ObjectType: policies.PlatformType, - Object: policies.MagistralaObject, - }, nil); err != nil { - return err - } - return nil -} diff --git a/re/middleware/authorization_test.go b/re/middleware/authorization_test.go deleted file mode 100644 index 155043e1f..000000000 --- a/re/middleware/authorization_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "testing" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - pkgerrors "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/mocks" - "github.com/absmach/magistrala/re/operations" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type recordingAtomAuthorizer struct { - allowed bool - reqs []atom.AuthzRequest -} - -func (a *recordingAtomAuthorizer) CheckAuthz(_ context.Context, req atom.AuthzRequest) (atom.AuthzResponse, error) { - a.reqs = append(a.reqs, req) - return atom.AuthzResponse{Allowed: a.allowed}, nil -} - -func TestListRulesAuthorizesRegularUser(t *testing.T) { - svc := mocks.NewService(t) - pm := re.PageMeta{Limit: 10} - session := authn.Session{UserID: "user-1", DomainID: "domain-1", DomainUserID: "domain-1_user-1"} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListRules", mock.Anything, session, pm).Return(re.Page{Limit: 10}, nil).Once() - page, err := wrapped.ListRules(context.Background(), session, pm) - - require.NoError(t, err) - assert.Equal(t, uint64(10), page.Limit) - require.Len(t, authz.reqs, 1) - assert.Equal(t, atom.AuthzRequest{ - SubjectID: "user-1", - Action: "list", - ResourceID: auth.AnyIDs, - ObjectKind: "resource", - ObjectID: auth.AnyIDs, - Context: map[string]any{ - "domain_id": "domain-1", - "legacy_object_type": operations.EntityType, - }, - }, authz.reqs[0]) -} - -func TestListRulesDeniedRegularUserDoesNotDelegate(t *testing.T) { - svc := mocks.NewService(t) - authz := &recordingAtomAuthorizer{allowed: false} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - _, err = wrapped.ListRules(context.Background(), authn.Session{UserID: "user-1", DomainID: "domain-1"}, re.PageMeta{}) - - assert.True(t, pkgerrors.Contains(err, pkgerrors.ErrAuthorization)) - require.Len(t, authz.reqs, 1) -} - -func TestListRulesSuperAdminSkipsListAuthorization(t *testing.T) { - svc := mocks.NewService(t) - pm := re.PageMeta{Limit: 10} - session := authn.Session{UserID: "admin-1", DomainID: "domain-1", Role: authn.SuperAdminRole} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListRules", mock.Anything, mock.MatchedBy(func(s authn.Session) bool { - return s.SuperAdmin - }), pm).Return(re.Page{Limit: 10}, nil).Once() - _, err = wrapped.ListRules(context.Background(), session, pm) - - require.NoError(t, err) - require.Len(t, authz.reqs, 1) - assert.Equal(t, "manage", authz.reqs[0].Action) -} - -func testEntitiesOps(t *testing.T) permissions.EntitiesOperations[permissions.Operation] { - t.Helper() - details := operations.OperationDetails() - perms := make(map[string]permissions.Permission, len(details)) - for _, detail := range details { - if detail.PermissionRequired { - perms[detail.Name] = permissions.Permission(detail.Name) - } - } - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{operations.EntityType: perms}, - permissions.EntitiesOperationDetails[permissions.Operation]{operations.EntityType: details}, - ) - require.NoError(t, err) - return entitiesOps -} diff --git a/re/middleware/callout.go b/re/middleware/callout.go deleted file mode 100644 index 8c46108ef..000000000 --- a/re/middleware/callout.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/callout" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/policies" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/operations" -) - -var _ re.Service = (*calloutMiddleware)(nil) - -type calloutMiddleware struct { - svc re.Service - callout callout.Callout - entitiesOps permissions.EntitiesOperations[permissions.Operation] -} - -const ( - entityIDKey = "entity_id" - entityType = "rule" -) - -func NewCallout(svc re.Service, callout callout.Callout, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (re.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - - return &calloutMiddleware{ - svc: svc, - callout: callout, - entitiesOps: entitiesOps, - }, nil -} - -func (cm *calloutMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - params := map[string]any{ - "entities": r, - "count": 1, - } - - if err := cm.callOut(ctx, session, operations.OpAddRule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.AddRule(ctx, session, r) -} - -func (cm *calloutMiddleware) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpViewRule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.ViewRule(ctx, session, id, withRoles) -} - -func (cm *calloutMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - params := map[string]any{ - entityIDKey: r.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateRule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.UpdateRule(ctx, session, r) -} - -func (cm *calloutMiddleware) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - params := map[string]any{ - entityIDKey: r.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateRuleTags, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.UpdateRuleTags(ctx, session, r) -} - -func (cm *calloutMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - params := map[string]any{ - entityIDKey: r.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateRuleSchedule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.UpdateRuleSchedule(ctx, session, r) -} - -func (cm *calloutMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - params := map[string]any{ - "pagemeta": pm, - } - - if err := cm.callOut(ctx, session, operations.OpListRules, params); err != nil { - return re.Page{}, err - } - - return cm.svc.ListRules(ctx, session, pm) -} - -func (cm *calloutMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) error { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpRemoveRule, params); err != nil { - return err - } - - return cm.svc.RemoveRule(ctx, session, id) -} - -func (cm *calloutMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpEnableRule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.EnableRule(ctx, session, id) -} - -func (cm *calloutMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpDisableRule, params); err != nil { - return re.Rule{}, err - } - - return cm.svc.DisableRule(ctx, session, id) -} - -func (cm *calloutMiddleware) StartScheduler(ctx context.Context) error { - return cm.svc.StartScheduler(ctx) -} - -func (cm *calloutMiddleware) Handle(msg *messaging.Message) error { - return cm.svc.Handle(msg) -} - -func (cm *calloutMiddleware) Cancel() error { - return cm.svc.Cancel() -} - -func (cm *calloutMiddleware) callOut(ctx context.Context, session authn.Session, op permissions.Operation, pld map[string]any) error { - var entityID string - if id, ok := pld[entityIDKey].(string); ok { - entityID = id - } - - req := callout.Request{ - BaseRequest: callout.BaseRequest{ - Operation: cm.entitiesOps.OperationName(entityType, op), - EntityType: entityType, - EntityID: entityID, - CallerID: session.UserID, - CallerType: policies.UserType, - DomainID: session.DomainID, - Time: time.Now().UTC(), - }, - Payload: pld, - } - - if err := cm.callout.Callout(ctx, req); err != nil { - return err - } - - return nil -} diff --git a/re/middleware/logging.go b/re/middleware/logging.go deleted file mode 100644 index 41e924594..000000000 --- a/re/middleware/logging.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "fmt" - "log/slog" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re" -) - -var _ re.Service = (*loggingMiddleware)(nil) - -type loggingMiddleware struct { - logger *slog.Logger - svc re.Service -} - -func LoggingMiddleware(svc re.Service, logger *slog.Logger) re.Service { - return &loggingMiddleware{ - logger: logger, - svc: svc, - } -} - -func (lm *loggingMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("rule_name", r.Name), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Add rule failed", args...) - return - } - lm.logger.Info("Add rule completed successfully", args...) - }(time.Now()) - res, err = lm.svc.AddRule(ctx, session, r) - return -} - -func (lm *loggingMiddleware) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("View rule failed", args...) - return - } - lm.logger.Info("View rule completed successfully", args...) - }(time.Now()) - return lm.svc.ViewRule(ctx, session, id, withRoles) -} - -func (lm *loggingMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", r.ID), - slog.String("name", r.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update rule failed", args...) - return - } - lm.logger.Info("Update rule completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateRule(ctx, session, r) -} - -func (lm *loggingMiddleware) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", r.ID), - slog.String("name", r.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update rule failed", args...) - return - } - lm.logger.Info("Update rule tags completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateRuleTags(ctx, session, r) -} - -func (lm *loggingMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", r.ID), - slog.Any("schedule", r.Schedule), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update rule schedule failed", args...) - return - } - lm.logger.Info("Update rule schedule completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateRuleSchedule(ctx, session, r) -} - -func (lm *loggingMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (pg re.Page, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("page", - slog.Uint64("offset", pm.Offset), - slog.Uint64("limit", pm.Limit), - slog.Uint64("total", pg.Total), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("List rules failed", args...) - return - } - lm.logger.Info("List rules completed successfully", args...) - }(time.Now()) - return lm.svc.ListRules(ctx, session, pm) -} - -func (lm *loggingMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("rule_id", id), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Remove rule failed", args...) - return - } - lm.logger.Info("Remove rule completed successfully", args...) - }(time.Now()) - return lm.svc.RemoveRule(ctx, session, id) -} - -func (lm *loggingMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Enable rule failed", args...) - return - } - lm.logger.Info("Enable rule completed successfully", args...) - }(time.Now()) - return lm.svc.EnableRule(ctx, session, id) -} - -func (lm *loggingMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (res re.Rule, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("rule", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Disable rule failed", args...) - return - } - lm.logger.Info("Disable rule completed successfully", args...) - }(time.Now()) - return lm.svc.DisableRule(ctx, session, id) -} - -func (lm *loggingMiddleware) StartScheduler(ctx context.Context) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Start scheduler failed", args...) - return - } - lm.logger.Info("Start scheduler completed successfully", args...) - }(time.Now()) - return lm.svc.StartScheduler(ctx) -} - -func (lm *loggingMiddleware) Handle(msg *messaging.Message) (err error) { - defer func(begin time.Time) { - // Log only failure since the handlers are executed async and will always - // return nil error. The rest of the loggin is performed in main.go error loop. - if err != nil { - args := []any{ - slog.String("duration", time.Since(begin).String()), - } - if msg != nil { - args = append(args, - slog.String("channel", msg.Channel), - slog.String("payload_size", fmt.Sprintf("%d", len(msg.Payload))), - ) - } - lm.logger.Warn("Message consumption completed", args...) - } - }(time.Now()) - - err = lm.svc.Handle(msg) - return -} - -func (lm *loggingMiddleware) Cancel() error { - return lm.svc.Cancel() -} diff --git a/re/middleware/metrics.go b/re/middleware/metrics.go deleted file mode 100644 index e776292f3..000000000 --- a/re/middleware/metrics.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re" - "github.com/go-kit/kit/metrics" -) - -type metricsMiddleware struct { - counter metrics.Counter - latency metrics.Histogram - service re.Service -} - -var _ re.Service = (*metricsMiddleware)(nil) - -func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service re.Service) re.Service { - return &metricsMiddleware{ - counter: counter, - latency: latency, - service: service, - } -} - -func (mm *metricsMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "add_rule").Add(1) - mm.latency.With("method", "add_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.AddRule(ctx, session, r) -} - -func (mm *metricsMiddleware) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "view_rule").Add(1) - mm.latency.With("method", "view_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ViewRule(ctx, session, id, withRoles) -} - -func (mm *metricsMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_rule").Add(1) - mm.latency.With("method", "update_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateRule(ctx, session, r) -} - -func (mm *metricsMiddleware) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_rule_tags").Add(1) - mm.latency.With("method", "update_rule_tags").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateRuleTags(ctx, session, r) -} - -func (mm *metricsMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_rule_schedule").Add(1) - mm.latency.With("method", "update_rule_schedule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateRuleSchedule(ctx, session, r) -} - -func (mm *metricsMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - defer func(begin time.Time) { - mm.counter.With("method", "list_rules").Add(1) - mm.latency.With("method", "list_rules").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ListRules(ctx, session, pm) -} - -func (mm *metricsMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) error { - defer func(begin time.Time) { - mm.counter.With("method", "remove_rule").Add(1) - mm.latency.With("method", "remove_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.RemoveRule(ctx, session, id) -} - -func (mm *metricsMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "enable_rule").Add(1) - mm.latency.With("method", "enable_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.EnableRule(ctx, session, id) -} - -func (mm *metricsMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - defer func(begin time.Time) { - mm.counter.With("method", "disable_rule").Add(1) - mm.latency.With("method", "disable_rule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.DisableRule(ctx, session, id) -} - -func (mm *metricsMiddleware) Handle(msg *messaging.Message) error { - defer func(begin time.Time) { - mm.counter.With("method", "handle").Add(1) - mm.latency.With("method", "handle").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.Handle(msg) -} - -func (mm *metricsMiddleware) StartScheduler(ctx context.Context) error { - defer func(begin time.Time) { - mm.counter.With("method", "start_scheduler").Add(1) - mm.latency.With("method", "start_scheduler").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.StartScheduler(ctx) -} - -func (mm *metricsMiddleware) Cancel() error { - return mm.service.Cancel() -} diff --git a/re/middleware/tracing.go b/re/middleware/tracing.go deleted file mode 100644 index 4ff6d1881..000000000 --- a/re/middleware/tracing.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/messaging" - smqTracing "github.com/absmach/magistrala/pkg/tracing" - "github.com/absmach/magistrala/re" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type tracingMiddleware struct { - tracer trace.Tracer - svc re.Service -} - -var _ re.Service = (*tracingMiddleware)(nil) - -func NewTracingMiddleware(tracer trace.Tracer, svc re.Service) re.Service { - return &tracingMiddleware{ - tracer: tracer, - svc: svc, - } -} - -func (tm *tracingMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "add_rule", trace.WithAttributes( - attribute.String("name", r.Name), - attribute.String("domain_id", r.DomainID), - )) - defer span.End() - - return tm.svc.AddRule(ctx, session, r) -} - -func (tm *tracingMiddleware) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "view_rule", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.ViewRule(ctx, session, id, withRoles) -} - -func (tm *tracingMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_rule", trace.WithAttributes( - attribute.String("id", r.ID), - )) - defer span.End() - - return tm.svc.UpdateRule(ctx, session, r) -} - -func (tm *tracingMiddleware) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_rule_tags", trace.WithAttributes( - attribute.String("id", r.ID), - )) - defer span.End() - - return tm.svc.UpdateRuleTags(ctx, session, r) -} - -func (tm *tracingMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_rule_schedule", trace.WithAttributes( - attribute.String("id", r.ID), - )) - defer span.End() - - return tm.svc.UpdateRuleSchedule(ctx, session, r) -} - -func (tm *tracingMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_rules", trace.WithAttributes( - attribute.Int("offset", int(pm.Offset)), - attribute.Int("limit", int(pm.Limit)), - )) - defer span.End() - - return tm.svc.ListRules(ctx, session, pm) -} - -func (tm *tracingMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "remove_rule", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.RemoveRule(ctx, session, id) -} - -func (tm *tracingMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "enable_rule", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.EnableRule(ctx, session, id) -} - -func (tm *tracingMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "disable_rule", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.DisableRule(ctx, session, id) -} - -func (tm *tracingMiddleware) Handle(msg *messaging.Message) error { - _, span := smqTracing.StartSpan(context.Background(), tm.tracer, "handle", trace.WithAttributes( - attribute.String("channel", msg.Channel), - attribute.String("subtopic", msg.Subtopic), - )) - defer span.End() - - return tm.svc.Handle(msg) -} - -func (tm *tracingMiddleware) StartScheduler(ctx context.Context) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "start_scheduler") - defer span.End() - - return tm.svc.StartScheduler(ctx) -} - -func (tm *tracingMiddleware) Cancel() error { - return tm.svc.Cancel() -} diff --git a/re/mocks/repository.go b/re/mocks/repository.go deleted file mode 100644 index 4bfe2844a..000000000 --- a/re/mocks/repository.go +++ /dev/null @@ -1,635 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - "time" - - "github.com/absmach/magistrala/re" - mock "github.com/stretchr/testify/mock" -) - -// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -type Repository_Expecter struct { - mock *mock.Mock -} - -func (_m *Repository) EXPECT() *Repository_Expecter { - return &Repository_Expecter{mock: &_m.Mock} -} - -// AddRule provides a mock function for the type Repository -func (_mock *Repository) AddRule(ctx context.Context, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, r) - - if len(ret) == 0 { - panic("no return value specified for AddRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok { - r1 = returnFunc(ctx, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_AddRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddRule' -type Repository_AddRule_Call struct { - *mock.Call -} - -// AddRule is a helper method to define mock.On call -// - ctx context.Context -// - r re.Rule -func (_e *Repository_Expecter) AddRule(ctx interface{}, r interface{}) *Repository_AddRule_Call { - return &Repository_AddRule_Call{Call: _e.mock.On("AddRule", ctx, r)} -} - -func (_c *Repository_AddRule_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_AddRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.Rule - if args[1] != nil { - arg1 = args[1].(re.Rule) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_AddRule_Call) Return(rule re.Rule, err error) *Repository_AddRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_AddRule_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_AddRule_Call { - _c.Call.Return(run) - return _c -} - -// ListAllRules provides a mock function for the type Repository -func (_mock *Repository) ListAllRules(ctx context.Context, pm re.PageMeta) (re.Page, error) { - ret := _mock.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for ListAllRules") - } - - var r0 re.Page - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.PageMeta) (re.Page, error)); ok { - return returnFunc(ctx, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.PageMeta) re.Page); ok { - r0 = returnFunc(ctx, pm) - } else { - r0 = ret.Get(0).(re.Page) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.PageMeta) error); ok { - r1 = returnFunc(ctx, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ListAllRules_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllRules' -type Repository_ListAllRules_Call struct { - *mock.Call -} - -// ListAllRules is a helper method to define mock.On call -// - ctx context.Context -// - pm re.PageMeta -func (_e *Repository_Expecter) ListAllRules(ctx interface{}, pm interface{}) *Repository_ListAllRules_Call { - return &Repository_ListAllRules_Call{Call: _e.mock.On("ListAllRules", ctx, pm)} -} - -func (_c *Repository_ListAllRules_Call) Run(run func(ctx context.Context, pm re.PageMeta)) *Repository_ListAllRules_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.PageMeta - if args[1] != nil { - arg1 = args[1].(re.PageMeta) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_ListAllRules_Call) Return(page re.Page, err error) *Repository_ListAllRules_Call { - _c.Call.Return(page, err) - return _c -} - -func (_c *Repository_ListAllRules_Call) RunAndReturn(run func(ctx context.Context, pm re.PageMeta) (re.Page, error)) *Repository_ListAllRules_Call { - _c.Call.Return(run) - return _c -} - -// RemoveRule provides a mock function for the type Repository -func (_mock *Repository) RemoveRule(ctx context.Context, id string) error { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for RemoveRule") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_RemoveRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRule' -type Repository_RemoveRule_Call struct { - *mock.Call -} - -// RemoveRule is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *Repository_Expecter) RemoveRule(ctx interface{}, id interface{}) *Repository_RemoveRule_Call { - return &Repository_RemoveRule_Call{Call: _e.mock.On("RemoveRule", ctx, id)} -} - -func (_c *Repository_RemoveRule_Call) Run(run func(ctx context.Context, id string)) *Repository_RemoveRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_RemoveRule_Call) Return(err error) *Repository_RemoveRule_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_RemoveRule_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_RemoveRule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRule provides a mock function for the type Repository -func (_mock *Repository) UpdateRule(ctx context.Context, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok { - r1 = returnFunc(ctx, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRule' -type Repository_UpdateRule_Call struct { - *mock.Call -} - -// UpdateRule is a helper method to define mock.On call -// - ctx context.Context -// - r re.Rule -func (_e *Repository_Expecter) UpdateRule(ctx interface{}, r interface{}) *Repository_UpdateRule_Call { - return &Repository_UpdateRule_Call{Call: _e.mock.On("UpdateRule", ctx, r)} -} - -func (_c *Repository_UpdateRule_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.Rule - if args[1] != nil { - arg1 = args[1].(re.Rule) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateRule_Call) Return(rule re.Rule, err error) *Repository_UpdateRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_UpdateRule_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleDue provides a mock function for the type Repository -func (_mock *Repository) UpdateRuleDue(ctx context.Context, id string, due time.Time) (re.Rule, error) { - ret := _mock.Called(ctx, id, due) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleDue") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) (re.Rule, error)); ok { - return returnFunc(ctx, id, due) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) re.Rule); ok { - r0 = returnFunc(ctx, id, due) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time) error); ok { - r1 = returnFunc(ctx, id, due) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateRuleDue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleDue' -type Repository_UpdateRuleDue_Call struct { - *mock.Call -} - -// UpdateRuleDue is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - due time.Time -func (_e *Repository_Expecter) UpdateRuleDue(ctx interface{}, id interface{}, due interface{}) *Repository_UpdateRuleDue_Call { - return &Repository_UpdateRuleDue_Call{Call: _e.mock.On("UpdateRuleDue", ctx, id, due)} -} - -func (_c *Repository_UpdateRuleDue_Call) Run(run func(ctx context.Context, id string, due time.Time)) *Repository_UpdateRuleDue_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 time.Time - if args[2] != nil { - arg2 = args[2].(time.Time) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_UpdateRuleDue_Call) Return(rule re.Rule, err error) *Repository_UpdateRuleDue_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_UpdateRuleDue_Call) RunAndReturn(run func(ctx context.Context, id string, due time.Time) (re.Rule, error)) *Repository_UpdateRuleDue_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleSchedule provides a mock function for the type Repository -func (_mock *Repository) UpdateRuleSchedule(ctx context.Context, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleSchedule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok { - r1 = returnFunc(ctx, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateRuleSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleSchedule' -type Repository_UpdateRuleSchedule_Call struct { - *mock.Call -} - -// UpdateRuleSchedule is a helper method to define mock.On call -// - ctx context.Context -// - r re.Rule -func (_e *Repository_Expecter) UpdateRuleSchedule(ctx interface{}, r interface{}) *Repository_UpdateRuleSchedule_Call { - return &Repository_UpdateRuleSchedule_Call{Call: _e.mock.On("UpdateRuleSchedule", ctx, r)} -} - -func (_c *Repository_UpdateRuleSchedule_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRuleSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.Rule - if args[1] != nil { - arg1 = args[1].(re.Rule) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateRuleSchedule_Call) Return(rule re.Rule, err error) *Repository_UpdateRuleSchedule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRuleSchedule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleStatus provides a mock function for the type Repository -func (_mock *Repository) UpdateRuleStatus(ctx context.Context, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleStatus") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok { - r1 = returnFunc(ctx, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateRuleStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleStatus' -type Repository_UpdateRuleStatus_Call struct { - *mock.Call -} - -// UpdateRuleStatus is a helper method to define mock.On call -// - ctx context.Context -// - r re.Rule -func (_e *Repository_Expecter) UpdateRuleStatus(ctx interface{}, r interface{}) *Repository_UpdateRuleStatus_Call { - return &Repository_UpdateRuleStatus_Call{Call: _e.mock.On("UpdateRuleStatus", ctx, r)} -} - -func (_c *Repository_UpdateRuleStatus_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRuleStatus_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.Rule - if args[1] != nil { - arg1 = args[1].(re.Rule) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateRuleStatus_Call) Return(rule re.Rule, err error) *Repository_UpdateRuleStatus_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_UpdateRuleStatus_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRuleStatus_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleTags provides a mock function for the type Repository -func (_mock *Repository) UpdateRuleTags(ctx context.Context, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleTags") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok { - r1 = returnFunc(ctx, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateRuleTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleTags' -type Repository_UpdateRuleTags_Call struct { - *mock.Call -} - -// UpdateRuleTags is a helper method to define mock.On call -// - ctx context.Context -// - r re.Rule -func (_e *Repository_Expecter) UpdateRuleTags(ctx interface{}, r interface{}) *Repository_UpdateRuleTags_Call { - return &Repository_UpdateRuleTags_Call{Call: _e.mock.On("UpdateRuleTags", ctx, r)} -} - -func (_c *Repository_UpdateRuleTags_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRuleTags_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 re.Rule - if args[1] != nil { - arg1 = args[1].(re.Rule) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateRuleTags_Call) Return(rule re.Rule, err error) *Repository_UpdateRuleTags_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_UpdateRuleTags_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRuleTags_Call { - _c.Call.Return(run) - return _c -} - -// ViewRule provides a mock function for the type Repository -func (_mock *Repository) ViewRule(ctx context.Context, id string) (re.Rule, error) { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for ViewRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (re.Rule, error)); ok { - return returnFunc(ctx, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) re.Rule); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = returnFunc(ctx, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ViewRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewRule' -type Repository_ViewRule_Call struct { - *mock.Call -} - -// ViewRule is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *Repository_Expecter) ViewRule(ctx interface{}, id interface{}) *Repository_ViewRule_Call { - return &Repository_ViewRule_Call{Call: _e.mock.On("ViewRule", ctx, id)} -} - -func (_c *Repository_ViewRule_Call) Run(run func(ctx context.Context, id string)) *Repository_ViewRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_ViewRule_Call) Return(rule re.Rule, err error) *Repository_ViewRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Repository_ViewRule_Call) RunAndReturn(run func(ctx context.Context, id string) (re.Rule, error)) *Repository_ViewRule_Call { - _c.Call.Return(run) - return _c -} diff --git a/re/mocks/service.go b/re/mocks/service.go deleted file mode 100644 index b53ff53d4..000000000 --- a/re/mocks/service.go +++ /dev/null @@ -1,836 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/re" - mock "github.com/stretchr/testify/mock" -) - -// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewService(t interface { - mock.TestingT - Cleanup(func()) -}) *Service { - mock := &Service{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Service is an autogenerated mock type for the Service type -type Service struct { - mock.Mock -} - -type Service_Expecter struct { - mock *mock.Mock -} - -func (_m *Service) EXPECT() *Service_Expecter { - return &Service_Expecter{mock: &_m.Mock} -} - -// AddRule provides a mock function for the type Service -func (_mock *Service) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, session, r) - - if len(ret) == 0 { - panic("no return value specified for AddRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, session, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, session, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.Rule) error); ok { - r1 = returnFunc(ctx, session, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_AddRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddRule' -type Service_AddRule_Call struct { - *mock.Call -} - -// AddRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - r re.Rule -func (_e *Service_Expecter) AddRule(ctx interface{}, session interface{}, r interface{}) *Service_AddRule_Call { - return &Service_AddRule_Call{Call: _e.mock.On("AddRule", ctx, session, r)} -} - -func (_c *Service_AddRule_Call) Run(run func(ctx context.Context, session authn.Session, r re.Rule)) *Service_AddRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 re.Rule - if args[2] != nil { - arg2 = args[2].(re.Rule) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_AddRule_Call) Return(rule re.Rule, err error) *Service_AddRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_AddRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error)) *Service_AddRule_Call { - _c.Call.Return(run) - return _c -} - -// Cancel provides a mock function for the type Service -func (_mock *Service) Cancel() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for Cancel") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_Cancel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cancel' -type Service_Cancel_Call struct { - *mock.Call -} - -// Cancel is a helper method to define mock.On call -func (_e *Service_Expecter) Cancel() *Service_Cancel_Call { - return &Service_Cancel_Call{Call: _e.mock.On("Cancel")} -} - -func (_c *Service_Cancel_Call) Run(run func()) *Service_Cancel_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *Service_Cancel_Call) Return(err error) *Service_Cancel_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_Cancel_Call) RunAndReturn(run func() error) *Service_Cancel_Call { - _c.Call.Return(run) - return _c -} - -// DisableRule provides a mock function for the type Service -func (_mock *Service) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DisableRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (re.Rule, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) re.Rule); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_DisableRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableRule' -type Service_DisableRule_Call struct { - *mock.Call -} - -// DisableRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) DisableRule(ctx interface{}, session interface{}, id interface{}) *Service_DisableRule_Call { - return &Service_DisableRule_Call{Call: _e.mock.On("DisableRule", ctx, session, id)} -} - -func (_c *Service_DisableRule_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DisableRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_DisableRule_Call) Return(rule re.Rule, err error) *Service_DisableRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_DisableRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (re.Rule, error)) *Service_DisableRule_Call { - _c.Call.Return(run) - return _c -} - -// EnableRule provides a mock function for the type Service -func (_mock *Service) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for EnableRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (re.Rule, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) re.Rule); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_EnableRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableRule' -type Service_EnableRule_Call struct { - *mock.Call -} - -// EnableRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) EnableRule(ctx interface{}, session interface{}, id interface{}) *Service_EnableRule_Call { - return &Service_EnableRule_Call{Call: _e.mock.On("EnableRule", ctx, session, id)} -} - -func (_c *Service_EnableRule_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_EnableRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_EnableRule_Call) Return(rule re.Rule, err error) *Service_EnableRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_EnableRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (re.Rule, error)) *Service_EnableRule_Call { - _c.Call.Return(run) - return _c -} - -// Handle provides a mock function for the type Service -func (_mock *Service) Handle(msg *messaging.Message) error { - ret := _mock.Called(msg) - - if len(ret) == 0 { - panic("no return value specified for Handle") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(*messaging.Message) error); ok { - r0 = returnFunc(msg) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_Handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handle' -type Service_Handle_Call struct { - *mock.Call -} - -// Handle is a helper method to define mock.On call -// - msg *messaging.Message -func (_e *Service_Expecter) Handle(msg interface{}) *Service_Handle_Call { - return &Service_Handle_Call{Call: _e.mock.On("Handle", msg)} -} - -func (_c *Service_Handle_Call) Run(run func(msg *messaging.Message)) *Service_Handle_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 *messaging.Message - if args[0] != nil { - arg0 = args[0].(*messaging.Message) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *Service_Handle_Call) Return(err error) *Service_Handle_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_Handle_Call) RunAndReturn(run func(msg *messaging.Message) error) *Service_Handle_Call { - _c.Call.Return(run) - return _c -} - -// ListRules provides a mock function for the type Service -func (_mock *Service) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) { - ret := _mock.Called(ctx, session, pm) - - if len(ret) == 0 { - panic("no return value specified for ListRules") - } - - var r0 re.Page - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.PageMeta) (re.Page, error)); ok { - return returnFunc(ctx, session, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.PageMeta) re.Page); ok { - r0 = returnFunc(ctx, session, pm) - } else { - r0 = ret.Get(0).(re.Page) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.PageMeta) error); ok { - r1 = returnFunc(ctx, session, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ListRules_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRules' -type Service_ListRules_Call struct { - *mock.Call -} - -// ListRules is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - pm re.PageMeta -func (_e *Service_Expecter) ListRules(ctx interface{}, session interface{}, pm interface{}) *Service_ListRules_Call { - return &Service_ListRules_Call{Call: _e.mock.On("ListRules", ctx, session, pm)} -} - -func (_c *Service_ListRules_Call) Run(run func(ctx context.Context, session authn.Session, pm re.PageMeta)) *Service_ListRules_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 re.PageMeta - if args[2] != nil { - arg2 = args[2].(re.PageMeta) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_ListRules_Call) Return(page re.Page, err error) *Service_ListRules_Call { - _c.Call.Return(page, err) - return _c -} - -func (_c *Service_ListRules_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error)) *Service_ListRules_Call { - _c.Call.Return(run) - return _c -} - -// RemoveRule provides a mock function for the type Service -func (_mock *Service) RemoveRule(ctx context.Context, session authn.Session, id string) error { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for RemoveRule") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_RemoveRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRule' -type Service_RemoveRule_Call struct { - *mock.Call -} - -// RemoveRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) RemoveRule(ctx interface{}, session interface{}, id interface{}) *Service_RemoveRule_Call { - return &Service_RemoveRule_Call{Call: _e.mock.On("RemoveRule", ctx, session, id)} -} - -func (_c *Service_RemoveRule_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_RemoveRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_RemoveRule_Call) Return(err error) *Service_RemoveRule_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_RemoveRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_RemoveRule_Call { - _c.Call.Return(run) - return _c -} - -// StartScheduler provides a mock function for the type Service -func (_mock *Service) StartScheduler(ctx context.Context) error { - ret := _mock.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for StartScheduler") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = returnFunc(ctx) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_StartScheduler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartScheduler' -type Service_StartScheduler_Call struct { - *mock.Call -} - -// StartScheduler is a helper method to define mock.On call -// - ctx context.Context -func (_e *Service_Expecter) StartScheduler(ctx interface{}) *Service_StartScheduler_Call { - return &Service_StartScheduler_Call{Call: _e.mock.On("StartScheduler", ctx)} -} - -func (_c *Service_StartScheduler_Call) Run(run func(ctx context.Context)) *Service_StartScheduler_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *Service_StartScheduler_Call) Return(err error) *Service_StartScheduler_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_StartScheduler_Call) RunAndReturn(run func(ctx context.Context) error) *Service_StartScheduler_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRule provides a mock function for the type Service -func (_mock *Service) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, session, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, session, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, session, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.Rule) error); ok { - r1 = returnFunc(ctx, session, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRule' -type Service_UpdateRule_Call struct { - *mock.Call -} - -// UpdateRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - r re.Rule -func (_e *Service_Expecter) UpdateRule(ctx interface{}, session interface{}, r interface{}) *Service_UpdateRule_Call { - return &Service_UpdateRule_Call{Call: _e.mock.On("UpdateRule", ctx, session, r)} -} - -func (_c *Service_UpdateRule_Call) Run(run func(ctx context.Context, session authn.Session, r re.Rule)) *Service_UpdateRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 re.Rule - if args[2] != nil { - arg2 = args[2].(re.Rule) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateRule_Call) Return(rule re.Rule, err error) *Service_UpdateRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_UpdateRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error)) *Service_UpdateRule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleSchedule provides a mock function for the type Service -func (_mock *Service) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, session, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleSchedule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, session, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, session, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.Rule) error); ok { - r1 = returnFunc(ctx, session, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateRuleSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleSchedule' -type Service_UpdateRuleSchedule_Call struct { - *mock.Call -} - -// UpdateRuleSchedule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - r re.Rule -func (_e *Service_Expecter) UpdateRuleSchedule(ctx interface{}, session interface{}, r interface{}) *Service_UpdateRuleSchedule_Call { - return &Service_UpdateRuleSchedule_Call{Call: _e.mock.On("UpdateRuleSchedule", ctx, session, r)} -} - -func (_c *Service_UpdateRuleSchedule_Call) Run(run func(ctx context.Context, session authn.Session, r re.Rule)) *Service_UpdateRuleSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 re.Rule - if args[2] != nil { - arg2 = args[2].(re.Rule) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateRuleSchedule_Call) Return(rule re.Rule, err error) *Service_UpdateRuleSchedule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error)) *Service_UpdateRuleSchedule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateRuleTags provides a mock function for the type Service -func (_mock *Service) UpdateRuleTags(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) { - ret := _mock.Called(ctx, session, r) - - if len(ret) == 0 { - panic("no return value specified for UpdateRuleTags") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) (re.Rule, error)); ok { - return returnFunc(ctx, session, r) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) re.Rule); ok { - r0 = returnFunc(ctx, session, r) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.Rule) error); ok { - r1 = returnFunc(ctx, session, r) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateRuleTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleTags' -type Service_UpdateRuleTags_Call struct { - *mock.Call -} - -// UpdateRuleTags is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - r re.Rule -func (_e *Service_Expecter) UpdateRuleTags(ctx interface{}, session interface{}, r interface{}) *Service_UpdateRuleTags_Call { - return &Service_UpdateRuleTags_Call{Call: _e.mock.On("UpdateRuleTags", ctx, session, r)} -} - -func (_c *Service_UpdateRuleTags_Call) Run(run func(ctx context.Context, session authn.Session, r re.Rule)) *Service_UpdateRuleTags_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 re.Rule - if args[2] != nil { - arg2 = args[2].(re.Rule) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateRuleTags_Call) Return(rule re.Rule, err error) *Service_UpdateRuleTags_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_UpdateRuleTags_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error)) *Service_UpdateRuleTags_Call { - _c.Call.Return(run) - return _c -} - -// ViewRule provides a mock function for the type Service -func (_mock *Service) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) { - ret := _mock.Called(ctx, session, id, withRoles) - - if len(ret) == 0 { - panic("no return value specified for ViewRule") - } - - var r0 re.Rule - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) (re.Rule, error)); ok { - return returnFunc(ctx, session, id, withRoles) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) re.Rule); ok { - r0 = returnFunc(ctx, session, id, withRoles) - } else { - r0 = ret.Get(0).(re.Rule) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string, bool) error); ok { - r1 = returnFunc(ctx, session, id, withRoles) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ViewRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewRule' -type Service_ViewRule_Call struct { - *mock.Call -} - -// ViewRule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -// - withRoles bool -func (_e *Service_Expecter) ViewRule(ctx interface{}, session interface{}, id interface{}, withRoles interface{}) *Service_ViewRule_Call { - return &Service_ViewRule_Call{Call: _e.mock.On("ViewRule", ctx, session, id, withRoles)} -} - -func (_c *Service_ViewRule_Call) Run(run func(ctx context.Context, session authn.Session, id string, withRoles bool)) *Service_ViewRule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - var arg3 bool - if args[3] != nil { - arg3 = args[3].(bool) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *Service_ViewRule_Call) Return(rule re.Rule, err error) *Service_ViewRule_Call { - _c.Call.Return(rule, err) - return _c -} - -func (_c *Service_ViewRule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error)) *Service_ViewRule_Call { - _c.Call.Return(run) - return _c -} diff --git a/re/operations/operations.go b/re/operations/operations.go deleted file mode 100644 index b41bd5c9e..000000000 --- a/re/operations/operations.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package operations - -import "github.com/absmach/magistrala/pkg/permissions" - -const EntityType = "rule" - -// Rule Operations. -const ( - OpAddRule permissions.Operation = iota - OpViewRule - OpUpdateRule - OpUpdateRuleTags - OpUpdateRuleSchedule - OpRemoveRule - OpListRules - OpEnableRule - OpDisableRule -) - -func OperationDetails() map[permissions.Operation]permissions.OperationDetails { - return map[permissions.Operation]permissions.OperationDetails{ - OpAddRule: { - Name: "create", - PermissionRequired: true, - }, - OpViewRule: { - Name: "view", - PermissionRequired: true, - }, - OpUpdateRule: { - Name: "update", - PermissionRequired: true, - }, - OpUpdateRuleTags: { - Name: "update_tags", - PermissionRequired: true, - }, - OpUpdateRuleSchedule: { - Name: "update_schedule", - PermissionRequired: true, - }, - OpRemoveRule: { - Name: "delete", - PermissionRequired: true, - }, - OpListRules: { - Name: "list", - PermissionRequired: true, - }, - OpEnableRule: { - Name: "enable", - PermissionRequired: true, - }, - OpDisableRule: { - Name: "disable", - PermissionRequired: true, - }, - } -} diff --git a/re/outputs/alarm.go b/re/outputs/alarm.go deleted file mode 100644 index 6b9ece1d5..000000000 --- a/re/outputs/alarm.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "bytes" - "context" - "encoding/gob" - "encoding/json" - - "github.com/absmach/magistrala/alarms" - "github.com/absmach/magistrala/pkg/messaging" -) - -const outputTypeKey = "type" - -type Alarm struct { - AlarmsPub messaging.Publisher `json:"-"` - RuleID string `json:"rule_id"` -} - -func (a *Alarm) Run(ctx context.Context, msg *messaging.Message, val any) error { - data, err := json.Marshal(val) - if err != nil { - return err - } - - var alarmsList []alarms.Alarm - if err := json.Unmarshal(data, &alarmsList); err != nil { - var single alarms.Alarm - if err := json.Unmarshal(data, &single); err != nil { - return err - } - alarmsList = []alarms.Alarm{single} - } - - for _, alarm := range alarmsList { - if err := a.processAlarm(ctx, msg, alarm); err != nil { - return err - } - } - - return nil -} - -func (a *Alarm) processAlarm(ctx context.Context, msg *messaging.Message, alarm alarms.Alarm) error { - alarm.RuleID = a.RuleID - alarm.DomainID = msg.Domain - alarm.ClientID = msg.ClientIdentity() - alarm.ChannelID = msg.Channel - alarm.Subtopic = msg.Subtopic - - var buf bytes.Buffer - if err := gob.NewEncoder(&buf).Encode(alarm); err != nil { - return err - } - - m := &messaging.Message{ - Domain: msg.Domain, - Publisher: msg.Publisher, - ClientId: msg.ClientIdentity(), - Created: msg.Created, - Channel: msg.Channel, - Subtopic: msg.Subtopic, - Protocol: msg.Protocol, - Payload: buf.Bytes(), - } - - topic := messaging.EncodeMessageTopic(msg) - if err := a.AlarmsPub.Publish(ctx, topic, m); err != nil { - return err - } - return nil -} - -func (a *Alarm) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - outputTypeKey: AlarmsType.String(), - }) -} diff --git a/re/outputs/channel.go b/re/outputs/channel.go deleted file mode 100644 index 11e6afe90..000000000 --- a/re/outputs/channel.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "context" - "encoding/json" - - "github.com/absmach/magistrala/pkg/messaging" -) - -type ChannelPublisher struct { - RePubSub messaging.PubSub `json:"-"` - Channel string `json:"channel"` - Topic string `json:"topic"` -} - -func (p *ChannelPublisher) Run(ctx context.Context, msg *messaging.Message, val any) error { - data, err := json.Marshal(val) - if err != nil { - return err - } - - m := &messaging.Message{ - Domain: msg.Domain, - Publisher: msg.Publisher, - ClientId: msg.ClientIdentity(), - Created: msg.Created, - Channel: p.Channel, - Subtopic: p.Topic, - Protocol: msg.Protocol, - Payload: data, - } - - topic := messaging.EncodeTopicSuffix(msg.Domain, p.Channel, p.Topic) - if err := p.RePubSub.Publish(ctx, topic, m); err != nil { - return err - } - - return nil -} - -func (cp *ChannelPublisher) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]string{ - outputTypeKey: ChannelsType.String(), - "channel": cp.Channel, - "topic": cp.Topic, - }) -} diff --git a/re/outputs/doc.go b/re/outputs/doc.go deleted file mode 100644 index 47f1ac5ff..000000000 --- a/re/outputs/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs diff --git a/re/outputs/email.go b/re/outputs/email.go deleted file mode 100644 index 6366b3198..000000000 --- a/re/outputs/email.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "bytes" - "context" - "encoding/json" - "text/template" - - "github.com/absmach/magistrala/pkg/emailer" - "github.com/absmach/magistrala/pkg/messaging" -) - -type Email struct { - To []string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` - Emailer emailer.Emailer `json:"-"` -} - -func (e *Email) Run(ctx context.Context, msg *messaging.Message, val any) error { - templData := templateVal{ - Message: msg, - Result: val, - } - - tmpl, err := template.New("email").Parse(e.Content) - if err != nil { - return err - } - - var output bytes.Buffer - if err := tmpl.Execute(&output, templData); err != nil { - return err - } - - content := output.String() - - if err := e.Emailer.SendEmailNotification(e.To, "", e.Subject, "", "", content, "", make(map[string][]byte)); err != nil { - return err - } - return nil -} - -func (e *Email) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - outputTypeKey: EmailType.String(), - "to": e.To, - "subject": e.Subject, - "content": e.Content, - }) -} diff --git a/re/outputs/outputs.go b/re/outputs/outputs.go deleted file mode 100644 index 4863c3065..000000000 --- a/re/outputs/outputs.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "encoding/json" - "strings" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/messaging" -) - -type templateVal struct { - Message *messaging.Message - Result any -} - -// OutputType is the indicator for type of the output -// so we can move it to the Go instead calling Go from Lua. -type OutputType uint - -const ( - ChannelsType OutputType = iota - AlarmsType - SaveSenMLType - EmailType - SaveRemotePgType - SlackType -) - -var ( - scriptKindToString = [...]string{"channels", "alarms", "save_senml", "email", "save_remote_pg", "slack"} - stringToScriptKind = map[string]OutputType{ - "channels": ChannelsType, - "alarms": AlarmsType, - "save_senml": SaveSenMLType, - "email": EmailType, - "save_remote_pg": SaveRemotePgType, - "slack": SlackType, - } -) - -func (s OutputType) String() string { - if int(s) < 0 || int(s) >= len(scriptKindToString) { - return "unknown" - } - return scriptKindToString[s] -} - -// MarshalJSON converts OutputType to JSON. -func (s *OutputType) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -// UnmarshalJSON parses JSON string into OutputType. -func (s *OutputType) UnmarshalJSON(data []byte) error { - var str string - if err := json.Unmarshal(data, &str); err != nil { - return err - } - lower := strings.ToLower(str) - if val, ok := stringToScriptKind[lower]; ok { - *s = val - return nil - } - return errors.New("invalid OutputType: " + str) -} diff --git a/re/outputs/postgres.go b/re/outputs/postgres.go deleted file mode 100644 index 080c4c449..000000000 --- a/re/outputs/postgres.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "strings" - "text/template" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/messaging" - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - "github.com/jmoiron/sqlx" -) - -type Postgres struct { - Host string `json:"host"` - Port int `json:"port"` - User string `json:"user"` - Password string `json:"password"` - Database string `json:"database"` - Table string `json:"table"` - Mapping string `json:"mapping"` -} - -func (p *Postgres) Run(ctx context.Context, msg *messaging.Message, val any) error { - templData := templateVal{ - Message: msg, - Result: val, - } - - tmpl, err := template.New("postgres").Parse(p.Mapping) - if err != nil { - return err - } - - var output bytes.Buffer - if err := tmpl.Execute(&output, templData); err != nil { - return err - } - - mapping := output.String() - var columns map[string]any - if err = json.Unmarshal([]byte(mapping), &columns); err != nil { - return err - } - - connStr := fmt.Sprintf( - "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", - p.Host, p.Port, p.User, p.Password, p.Database, - ) - - db, err := sqlx.Open("pgx", connStr) - if err != nil { - return err - } - defer db.Close() - - if err := db.Ping(); err != nil { - return errors.Wrap(errors.New("failed to connect to DB"), err) - } - - var ( - cols []string - values []any - placeholders []string - ) - - i := 1 - for k, v := range columns { - cols = append(cols, k) - values = append(values, v) - placeholders = append(placeholders, fmt.Sprintf("$%d", i)) - i++ - } - - q := fmt.Sprintf( - `INSERT INTO %s (%s) VALUES (%s)`, - p.Table, - strings.Join(cols, ", "), - strings.Join(placeholders, ", "), - ) - - _, err = db.Exec(q, values...) - if err != nil { - return errors.Wrap(errors.New("failed to insert data"), err) - } - - return nil -} - -func (p *Postgres) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - outputTypeKey: SaveRemotePgType.String(), - "host": p.Host, - "port": p.Port, - "user": p.User, - "password": p.Password, - "database": p.Database, - "table": p.Table, - "mapping": p.Mapping, - }) -} diff --git a/re/outputs/senml.go b/re/outputs/senml.go deleted file mode 100644 index 4194d61af..000000000 --- a/re/outputs/senml.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "context" - "encoding/json" - - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/senml" -) - -type SenML struct { - WritersPub messaging.Publisher `json:"-"` -} - -func (s *SenML) Run(ctx context.Context, msg *messaging.Message, val any) error { - // In case there is a single SenML value, convert to slice so we can decode. - if _, ok := val.([]any); !ok { - val = []any{val} - } - data, err := json.Marshal(val) - if err != nil { - return err - } - if _, err := senml.Decode(data, senml.JSON); err != nil { - return err - } - - m := &messaging.Message{ - Domain: msg.Domain, - Publisher: msg.Publisher, - ClientId: msg.ClientIdentity(), - Created: msg.Created, - Channel: msg.Channel, - Subtopic: msg.Subtopic, - Protocol: msg.Protocol, - Payload: data, - } - topic := messaging.EncodeMessageTopic(msg) - if err := s.WritersPub.Publish(ctx, topic, m); err != nil { - return err - } - - return nil -} - -func (senml *SenML) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]string{ - outputTypeKey: SaveSenMLType.String(), - }) -} diff --git a/re/outputs/slack.go b/re/outputs/slack.go deleted file mode 100644 index ee475fb26..000000000 --- a/re/outputs/slack.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package outputs - -import ( - "bytes" - "context" - "encoding/json" - "text/template" - - "github.com/absmach/magistrala/pkg/messaging" - "github.com/slack-go/slack" -) - -type Slack struct { - Token string `json:"token"` - ChannelID string `json:"channel_id"` - Message string `json:"message"` -} - -func (s *Slack) Run(ctx context.Context, msg *messaging.Message, val any) error { - templData := templateVal{ - Message: msg, - Result: val, - } - - tmpl, err := template.New("slack").Parse(s.Message) - if err != nil { - return err - } - - var output bytes.Buffer - if err := tmpl.Execute(&output, templData); err != nil { - return err - } - - mapping := output.String() - - var message slack.Msg - if err := json.Unmarshal([]byte(mapping), &message); err != nil { - return err - } - - slackClient := slack.New(s.Token) - - var opts []slack.MsgOption - - if message.Text != "" { - opts = append(opts, slack.MsgOptionText(message.Text, false)) - } - if len(message.Attachments) > 0 { - opts = append(opts, slack.MsgOptionAttachments(message.Attachments...)) - } - if len(message.Blocks.BlockSet) > 0 { - opts = append(opts, slack.MsgOptionBlocks(message.Blocks.BlockSet...)) - } - _, _, err = slackClient.PostMessage(s.ChannelID, opts...) - if err != nil { - return err - } - return nil -} - -func (s *Slack) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - outputTypeKey: SlackType.String(), - "token": s.Token, - "channel_id": s.ChannelID, - "message": s.Message, - }) -} diff --git a/re/postgres/init.go b/re/postgres/init.go deleted file mode 100644 index 099695b59..000000000 --- a/re/postgres/init.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - migrate "github.com/rubenv/sql-migrate" -) - -func Migration() (*migrate.MemoryMigrationSource, error) { - rulesMigration := &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "rules_01", - // VARCHAR(36) for colums with IDs as UUIDS have a maximum of 36 characters - // STATUS 0 to imply enabled and 1 to imply disabled - Up: []string{ - `CREATE TABLE IF NOT EXISTS rules ( - id VARCHAR(36) PRIMARY KEY, - name VARCHAR(1024), - domain_id VARCHAR(36) NOT NULL, - metadata JSONB, - created_by VARCHAR(254), - created_at TIMESTAMP, - updated_at TIMESTAMP, - updated_by VARCHAR(254), - input_channel VARCHAR(36), - input_topic TEXT, - outputs JSONB, - status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), - logic_type SMALLINT NOT NULL DEFAULT 0 CHECK (logic_type >= 0), - logic_value BYTEA, - time TIMESTAMP, - recurring SMALLINT, - recurring_period SMALLINT, - start_datetime TIMESTAMP - )`, - }, - Down: []string{ - `DROP TABLE IF EXISTS rules`, - }, - }, - { - Id: "rules_02", - Up: []string{ - `ALTER TABLE rules ADD COLUMN tags TEXT[];`, - }, - Down: []string{ - `ALTER TABLE rules DROP COLUMN tags;`, - }, - }, - { - Id: "rules_03", - Up: []string{ - `UPDATE rules - SET metadata = (COALESCE(metadata, '{}'::jsonb) - 'ui') || jsonb_build_object('flow', metadata->'ui') - WHERE metadata ? 'ui' AND jsonb_typeof(metadata->'ui') = 'string'`, - }, - Down: []string{ - `UPDATE rules - SET metadata = (COALESCE(metadata, '{}'::jsonb) - 'flow') || jsonb_build_object('ui', metadata->'flow') - WHERE metadata ? 'flow' AND jsonb_typeof(metadata->'flow') = 'string'`, - }, - }, - { - Id: "rules_04", - Up: []string{ - // Canonicalize legacy rule topics from dot/NATS wildcards - // to slash/MQTT wildcards. - `UPDATE rules - SET input_topic = REPLACE(REPLACE(REPLACE(input_topic, '>', '#'), '*', '+'), '.', '/') - WHERE input_topic IS NOT NULL AND input_topic <> ''`, - }, - Down: []string{ - `UPDATE rules - SET input_topic = REPLACE(REPLACE(REPLACE(input_topic, '#', '>'), '+', '*'), '/', '.') - WHERE input_topic IS NOT NULL AND input_topic <> ''`, - }, - }, - { - Id: "rules_05", - Up: []string{ - // Canonicalize channel output topics in rule outputs JSON: - // dot/NATS wildcards -> slash/MQTT wildcards. - `UPDATE rules AS r - SET outputs = COALESCE(( - SELECT jsonb_agg( - CASE - WHEN out_elem.elem->>'type' = 'channels' - AND out_elem.elem ? 'topic' - AND jsonb_typeof(out_elem.elem->'topic') = 'string' - THEN jsonb_set( - out_elem.elem, - '{topic}', - to_jsonb(REPLACE(REPLACE(REPLACE(out_elem.elem->>'topic', '>', '#'), '*', '+'), '.', '/')), - false - ) - ELSE out_elem.elem - END - ORDER BY out_elem.ord - ) - FROM jsonb_array_elements(r.outputs) WITH ORDINALITY AS out_elem(elem, ord) - ), '[]'::jsonb) - WHERE jsonb_typeof(r.outputs) = 'array'`, - }, - Down: []string{ - `UPDATE rules AS r - SET outputs = COALESCE(( - SELECT jsonb_agg( - CASE - WHEN out_elem.elem->>'type' = 'channels' - AND out_elem.elem ? 'topic' - AND jsonb_typeof(out_elem.elem->'topic') = 'string' - THEN jsonb_set( - out_elem.elem, - '{topic}', - to_jsonb(REPLACE(REPLACE(REPLACE(out_elem.elem->>'topic', '#', '>'), '+', '*'), '/', '.')), - false - ) - ELSE out_elem.elem - END - ORDER BY out_elem.ord - ) - FROM jsonb_array_elements(r.outputs) WITH ORDINALITY AS out_elem(elem, ord) - ), '[]'::jsonb) - WHERE jsonb_typeof(r.outputs) = 'array'`, - }, - }, - }, - } - - return rulesMigration, nil -} diff --git a/re/postgres/repository.go b/re/postgres/repository.go deleted file mode 100644 index c05e09623..000000000 --- a/re/postgres/repository.go +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "database/sql" - "fmt" - "strings" - "time" - - api "github.com/absmach/magistrala/api/http" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/re" -) - -type PostgresRepository struct { - DB postgres.Database -} - -func NewRepository(db postgres.Database) re.Repository { - return &PostgresRepository{ - DB: db, - } -} - -func (repo *PostgresRepository) AddRule(ctx context.Context, r re.Rule) (re.Rule, error) { - q := ` - INSERT INTO rules (id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status) - VALUES (:id, :name, :domain_id, :tags, :metadata, :input_channel, :input_topic, :logic_type, :logic_value, - :outputs, :start_datetime, :time, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status) - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; -` - dbr, err := ruleToDb(r) - if err != nil { - return re.Rule{}, err - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return re.Rule{}, postgres.HandleError(repoerr.ErrCreateEntity, err) - } - defer row.Close() - - var dbRule dbRule - if row.Next() { - if err := row.StructScan(&dbRule); err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err) - } - } - - rule, err := dbToRule(dbRule) - if err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err) - } - - return rule, nil -} - -func (repo *PostgresRepository) ViewRule(ctx context.Context, id string) (re.Rule, error) { - q := ` - SELECT id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, outputs, - start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status - FROM rules - WHERE id = $1; - ` - row := repo.DB.QueryRowxContext(ctx, q, id) - if err := row.Err(); err != nil { - return re.Rule{}, postgres.HandleError(repoerr.ErrViewEntity, err) - } - var dbr dbRule - if err := row.StructScan(&dbr); err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - ret, err := dbToRule(dbr) - if err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - - return ret, nil -} - -func (repo *PostgresRepository) UpdateRuleStatus(ctx context.Context, r re.Rule) (re.Rule, error) { - q := `UPDATE rules - SET status = :status, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;` - - return repo.update(ctx, r, q) -} - -func (repo *PostgresRepository) UpdateRule(ctx context.Context, r re.Rule) (re.Rule, error) { - var query []string - var upq string - if r.Name != "" { - query = append(query, "name = :name,") - } - if r.Metadata != nil { - query = append(query, "metadata = :metadata,") - } - query = append(query, "input_channel = :input_channel,") - query = append(query, "input_topic = :input_topic,") - if r.Outputs != nil { - query = append(query, "outputs = :outputs, ") - } - if r.Logic.Value != "" { - query = append(query, "logic_type = :logic_type,") - query = append(query, "logic_value = :logic_value,") - } - - if len(query) > 0 { - upq = strings.Join(query, " ") - } - - q := fmt.Sprintf(` - UPDATE rules - SET %s updated_at = :updated_at, updated_by = :updated_by WHERE id = :id - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - `, upq) - - return repo.update(ctx, r, q) -} - -func (repo *PostgresRepository) UpdateRuleTags(ctx context.Context, r re.Rule) (re.Rule, error) { - q := `UPDATE rules SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;` - r.Status = re.EnabledStatus - - return repo.update(ctx, r, q) -} - -func (repo *PostgresRepository) UpdateRuleSchedule(ctx context.Context, r re.Rule) (re.Rule, error) { - q := ` - UPDATE rules - SET start_datetime = :start_datetime, time = :time, recurring = :recurring, - recurring_period = :recurring_period, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - return repo.update(ctx, r, q) -} - -func (repo *PostgresRepository) update(ctx context.Context, r re.Rule, query string) (re.Rule, error) { - dbr, err := ruleToDb(r) - if err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - row, err := repo.DB.NamedQueryContext(ctx, query, dbr) - if err != nil { - return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - if !row.Next() { - return re.Rule{}, repoerr.ErrNotFound - } - var dbRule dbRule - if err := row.StructScan(&dbRule); err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - rule, err := dbToRule(dbRule) - if err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - return rule, nil -} - -func (repo *PostgresRepository) RemoveRule(ctx context.Context, id string) error { - q := ` - DELETE FROM rules - WHERE id = $1; -` - result, err := repo.DB.ExecContext(ctx, q, id) - if err != nil { - return postgres.HandleError(repoerr.ErrRemoveEntity, err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return errors.Wrap(repoerr.ErrRemoveEntity, err) - } - - if rowsAffected == 0 { - return repoerr.ErrNotFound - } - - return nil -} - -func (repo *PostgresRepository) ListAllRules(ctx context.Context, pm re.PageMeta) (re.Page, error) { - pq := pageRulesQuery(pm) - orderClause := rulesOrderClause(pm) - pgData := rulesPageData(pm) - - q := fmt.Sprintf(` - SELECT id, name, domain_id, tags, input_channel, input_topic, logic_type, logic_value, outputs, - start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status - FROM rules r %s %s %s; - `, pq, orderClause, pgData) - rows, err := repo.DB.NamedQueryContext(ctx, q, pm) - if err != nil { - return re.Page{}, err - } - defer rows.Close() - - var rules []re.Rule - var r dbRule - for rows.Next() { - if err := rows.StructScan(&r); err != nil { - return re.Page{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - ret, err := dbToRule(r) - if err != nil { - return re.Page{}, err - } - rules = append(rules, ret) - } - - cq := fmt.Sprintf(`SELECT COUNT(*) FROM rules r %s;`, pq) - - total, err := postgres.Total(ctx, repo.DB, cq, pm) - if err != nil { - return re.Page{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - ret := re.Page{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - Rules: rules, - } - - return ret, nil -} - -func (repo *PostgresRepository) UpdateRuleDue(ctx context.Context, id string, due time.Time) (re.Rule, error) { - q := ` - UPDATE rules - SET time = :time, updated_at = :updated_at WHERE id = :id - RETURNING id, name, domain_id, tags, metadata, input_channel, input_topic, logic_type, logic_value, - outputs, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - dbr := dbRule{ - ID: id, - UpdatedAt: time.Now().UTC(), - Time: sql.NullTime{Time: due}, - } - if !due.IsZero() { - dbr.Time.Valid = true - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - var dbRule dbRule - if row.Next() { - if err := row.StructScan(&dbRule); err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - } - rule, err := dbToRule(dbRule) - if err != nil { - return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - return rule, nil -} - -func rulesOrderClause(pm re.PageMeta) string { - dir := api.DescDir - if pm.Dir == api.AscDir { - dir = api.AscDir - } - - switch pm.Order { - case api.NameKey: - return fmt.Sprintf("ORDER BY name %s, id %s", dir, dir) - case api.CreatedAtOrder: - return fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir) - default: - return fmt.Sprintf("ORDER BY COALESCE(updated_at, created_at) %s, id %s", dir, dir) - } -} - -func rulesPageData(pm re.PageMeta) string { - pgData := "" - if pm.Limit != 0 { - pgData = "LIMIT :limit" - } - if pm.Offset != 0 { - pgData += " OFFSET :offset" - } - return pgData -} - -func pageRulesQuery(pm re.PageMeta) string { - query := pageRulesQueryConditions(pm) - var q string - if len(query) > 0 { - q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - - return q -} - -func pageRulesQueryConditions(pm re.PageMeta) []string { - var query []string - if pm.InputChannel != "" { - query = append(query, "r.input_channel = :input_channel") - } - if pm.Status != re.AllStatus { - query = append(query, "r.status = :status") - } - if pm.Domain != "" { - query = append(query, "r.domain_id = :domain_id") - } - if pm.Tag != "" { - query = append(query, "EXISTS (SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE '%' || :tag || '%')") - } - if pm.ScheduledBefore != nil { - query = append(query, "r.time < :scheduled_before") - } - if pm.ScheduledAfter != nil { - query = append(query, "r.time > :scheduled_after") - } - if pm.Name != "" { - query = append(query, "r.name ILIKE '%' || :name || '%'") - } - if pm.Scheduled != nil && !*pm.Scheduled { - query = append(query, "r.time IS NULL") - } - return query -} diff --git a/re/postgres/repository_test.go b/re/postgres/repository_test.go deleted file mode 100644 index 635f16fb2..000000000 --- a/re/postgres/repository_test.go +++ /dev/null @@ -1,998 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "sort" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/outputs" - "github.com/absmach/magistrala/re/postgres" - "github.com/stretchr/testify/assert" -) - -const ( - ascDir = "asc" - descDir = "desc" - nameOrder = "name" - createdAtOrder = "created_at" - updatedAtOrder = "updated_at" -) - -var ( - namegen = namegenerator.NewGenerator() - idProvider = uuid.New() -) - -func TestAddRule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - Tags: []string{"test", "rule"}, - InputChannel: generateUUID(t), - InputTopic: "temperature", - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Outputs: re.Outputs{ - &outputs.Alarm{}, - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - Metadata: map[string]any{ - "key": "value", - }, - } - - scheduleName := namegen.Generate() - scheduleDomain := generateUUID(t) - scheduleChannel := generateUUID(t) - scheduleCreatedBy := generateUUID(t) - scheduleCreatedAt := time.Now().UTC().Truncate(time.Microsecond) - scheduleUpdatedBy := generateUUID(t) - scheduleUpdatedAt := time.Now().UTC().Truncate(time.Microsecond) - scheduleStartTime := time.Now().UTC().Add(time.Hour).Truncate(time.Microsecond) - scheduleTime := time.Now().UTC().Add(2 * time.Hour).Truncate(time.Microsecond) - - scheduleRule := re.Rule{ - ID: generateUUID(t), - Name: scheduleName, - DomainID: scheduleDomain, - InputChannel: scheduleChannel, - InputTopic: "humidity", - Logic: re.Script{ - Type: re.LuaType, - Value: "return value > 50", - }, - Schedule: schedule.Schedule{ - StartDateTime: scheduleStartTime, - Time: scheduleTime, - Recurring: schedule.Daily, - RecurringPeriod: 1, - }, - Status: re.EnabledStatus, - CreatedAt: scheduleCreatedAt, - CreatedBy: scheduleCreatedBy, - UpdatedAt: scheduleUpdatedAt, - UpdatedBy: scheduleUpdatedBy, - Metadata: re.Metadata{}, - } - - outputsName := namegen.Generate() - outputsDomain := generateUUID(t) - outputsChannel := generateUUID(t) - outputsCreatedBy := generateUUID(t) - outputsCreatedAt := time.Now().UTC().Truncate(time.Microsecond) - outputsUpdatedBy := generateUUID(t) - outputsUpdatedAt := time.Now().UTC().Truncate(time.Microsecond) - outputsRuleID := generateUUID(t) - - outputsRule := re.Rule{ - ID: outputsRuleID, - Name: outputsName, - DomainID: outputsDomain, - InputChannel: outputsChannel, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: generateUUID(t), - Topic: "alerts", - }, - &outputs.SenML{}, - }, - Status: re.EnabledStatus, - CreatedAt: outputsCreatedAt, - CreatedBy: outputsCreatedBy, - UpdatedAt: outputsUpdatedAt, - UpdatedBy: outputsUpdatedBy, - Metadata: re.Metadata{}, - } - - cases := []struct { - desc string - rule re.Rule - resp re.Rule - err error - }{ - { - desc: "valid rule", - rule: rule, - resp: rule, - err: nil, - }, - { - desc: "duplicate rule", - rule: rule, - resp: re.Rule{}, - err: repoerr.ErrConflict, - }, - - { - desc: "rule with schedule", - rule: scheduleRule, - resp: scheduleRule, - err: nil, - }, - { - desc: "rule with outputs", - rule: outputsRule, - resp: outputsRule, - err: nil, - }, - { - desc: "invalid metadata", - rule: re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Metadata: map[string]any{ - "key": make(chan int), - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - resp: re.Rule{}, - err: repoerr.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - addedRule, err := repo.AddRule(context.Background(), tc.rule) - if err == nil { - tc.resp.ID = addedRule.ID - assert.Equal(t, tc.resp, addedRule, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, addedRule)) - } - }) - } -} - -func TestViewRule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - InputTopic: "temperature", - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - Metadata: map[string]any{ - "key": "value", - }, - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - resp re.Rule - err error - }{ - { - desc: "valid rule", - id: rule.ID, - resp: rule, - err: nil, - }, - { - desc: "non existing rule", - id: generateUUID(t), - resp: re.Rule{}, - err: repoerr.ErrViewEntity, - }, - { - desc: "empty id", - id: "", - resp: re.Rule{}, - err: repoerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - retrievedRule, err := repo.ViewRule(context.Background(), tc.id) - assert.Equal(t, tc.resp, retrievedRule, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, retrievedRule)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - }) - } -} - -func TestUpdateRule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - InputTopic: "temperature", - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - Metadata: map[string]any{ - "key": "value", - }, - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - newInputChannel := generateUUID(t) - newUpdatedBy := generateUUID(t) - - cases := []struct { - desc string - rule re.Rule - resp re.Rule - err error - }{ - { - desc: "valid rule update", - rule: re.Rule{ - ID: rule.ID, - Name: "updated-name", - InputChannel: newInputChannel, - InputTopic: "humidity", - Logic: re.Script{ - Type: re.LuaType, - Value: "return value > 30", - }, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: newUpdatedBy, - Metadata: map[string]any{ - "updated": "metadata", - }, - }, - resp: re.Rule{ - ID: rule.ID, - Name: "updated-name", - DomainID: rule.DomainID, - InputChannel: newInputChannel, - InputTopic: "humidity", - Logic: re.Script{ - Type: re.LuaType, - Value: "return value > 30", - }, - Status: rule.Status, - CreatedAt: rule.CreatedAt, - CreatedBy: rule.CreatedBy, - UpdatedAt: time.Time{}, - UpdatedBy: newUpdatedBy, - Metadata: map[string]any{ - "updated": "metadata", - }, - }, - err: nil, - }, - { - desc: "update non-existing rule", - rule: re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - InputChannel: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - resp: re.Rule{}, - err: repoerr.ErrNotFound, - }, - { - desc: "update with invalid metadata", - rule: re.Rule{ - ID: rule.ID, - InputChannel: generateUUID(t), - Metadata: map[string]any{ - "key": make(chan int), - }, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - resp: re.Rule{}, - err: repoerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - updatedRule, err := repo.UpdateRule(context.Background(), tc.rule) - if tc.err == nil { - tc.resp.UpdatedAt = updatedRule.UpdatedAt - } - assert.Equal(t, tc.resp, updatedRule, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, updatedRule)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - }) - } -} - -func TestUpdateRuleStatus(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - rule re.Rule - status re.Status - err error - }{ - { - desc: "disable rule", - rule: re.Rule{ - ID: rule.ID, - Status: re.DisabledStatus, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - status: re.DisabledStatus, - err: nil, - }, - { - desc: "enable rule", - rule: re.Rule{ - ID: rule.ID, - Status: re.EnabledStatus, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - status: re.EnabledStatus, - err: nil, - }, - { - desc: "update non-existing rule status", - rule: re.Rule{ - ID: generateUUID(t), - Status: re.DisabledStatus, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - updatedRule, err := repo.UpdateRuleStatus(context.Background(), tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.rule.ID, updatedRule.ID, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.ID, updatedRule.ID)) - assert.Equal(t, tc.status, updatedRule.Status, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.status, updatedRule.Status)) - assert.Equal(t, tc.rule.UpdatedBy, updatedRule.UpdatedBy, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.UpdatedBy, updatedRule.UpdatedBy)) - } - }) - } -} - -func TestUpdateRuleTags(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Tags: []string{"tag1", "tag2"}, - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - rule re.Rule - tags []string - err error - }{ - { - desc: "update tags", - rule: re.Rule{ - ID: rule.ID, - Tags: []string{"newtag1", "newtag2", "newtag3"}, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - tags: []string{"newtag1", "newtag2", "newtag3"}, - err: nil, - }, - { - desc: "update non-existing rule tags", - rule: re.Rule{ - ID: generateUUID(t), - Tags: []string{"tag"}, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - updatedRule, err := repo.UpdateRuleTags(context.Background(), tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.rule.ID, updatedRule.ID, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.ID, updatedRule.ID)) - assert.Equal(t, tc.tags, updatedRule.Tags, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.tags, updatedRule.Tags)) - assert.Equal(t, tc.rule.UpdatedBy, updatedRule.UpdatedBy, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.UpdatedBy, updatedRule.UpdatedBy)) - } - }) - } -} - -func TestUpdateRuleSchedule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - newSchedule := schedule.Schedule{ - StartDateTime: time.Now().UTC().Add(time.Hour).Truncate(time.Microsecond), - Time: time.Now().UTC().Add(2 * time.Hour).Truncate(time.Microsecond), - Recurring: schedule.Weekly, - RecurringPeriod: 2, - } - - cases := []struct { - desc string - rule re.Rule - schedule schedule.Schedule - err error - }{ - { - desc: "update schedule", - rule: re.Rule{ - ID: rule.ID, - Schedule: newSchedule, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - schedule: newSchedule, - err: nil, - }, - { - desc: "update non-existing rule schedule", - rule: re.Rule{ - ID: generateUUID(t), - Schedule: newSchedule, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - updatedRule, err := repo.UpdateRuleSchedule(context.Background(), tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.rule.ID, updatedRule.ID, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.ID, updatedRule.ID)) - assert.Equal(t, tc.schedule.Recurring, updatedRule.Schedule.Recurring, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.schedule.Recurring, updatedRule.Schedule.Recurring)) - assert.Equal(t, tc.schedule.RecurringPeriod, updatedRule.Schedule.RecurringPeriod, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.schedule.RecurringPeriod, updatedRule.Schedule.RecurringPeriod)) - assert.Equal(t, tc.rule.UpdatedBy, updatedRule.UpdatedBy, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.rule.UpdatedBy, updatedRule.UpdatedBy)) - } - }) - } -} - -func TestUpdateRuleDue(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Schedule: schedule.Schedule{ - Time: time.Now().UTC().Add(time.Hour).Truncate(time.Microsecond), - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - newDue := time.Now().UTC().Add(3 * time.Hour).Truncate(time.Microsecond) - - cases := []struct { - desc string - id string - due time.Time - err error - }{ - { - desc: "update due time", - id: rule.ID, - due: newDue, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - updatedRule, err := repo.UpdateRuleDue(context.Background(), tc.id, tc.due) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.id, updatedRule.ID, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.id, updatedRule.ID)) - assert.True(t, updatedRule.Schedule.Time.Sub(tc.due) < time.Second, fmt.Sprintf("%s: expected due time close to %v got %v\n", tc.desc, tc.due, updatedRule.Schedule.Time)) - } - }) - } -} - -func TestListRules(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - domainID := generateUUID(t) - channelID := generateUUID(t) - items := make([]re.Rule, 100) - - for i := range 100 { - items[i] = re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - InputChannel: channelID, - Tags: []string{fmt.Sprintf("tag%d", i%10)}, - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Add(time.Duration(i) * time.Minute).Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Add(time.Duration(i) * time.Minute).Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - if i%2 == 0 { - items[i].Status = re.DisabledStatus - } - if i%3 == 0 { - items[i].Schedule = schedule.Schedule{ - Time: time.Now().UTC().Add(time.Duration(i) * time.Hour), - Recurring: schedule.Daily, - } - } - rule, err := repo.AddRule(context.Background(), items[i]) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - items[i].ID = rule.ID - } - - cases := []struct { - desc string - pm re.PageMeta - count int - err error - }{ - { - desc: "list first page", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - }, - count: 10, - err: nil, - }, - { - desc: "list with offset", - pm: re.PageMeta{ - Offset: 10, - Limit: 20, - Status: re.AllStatus, - }, - count: 20, - err: nil, - }, - { - desc: "list by domain", - pm: re.PageMeta{ - Domain: domainID, - Offset: 0, - Limit: 200, - Status: re.AllStatus, - }, - count: 100, - err: nil, - }, - { - desc: "list by channel", - pm: re.PageMeta{ - InputChannel: channelID, - Offset: 0, - Limit: 200, - Status: re.AllStatus, - }, - count: 100, - err: nil, - }, - { - desc: "list enabled rules", - pm: re.PageMeta{ - Status: re.EnabledStatus, - Offset: 0, - Limit: 200, - }, - count: 50, - err: nil, - }, - { - desc: "list disabled rules", - pm: re.PageMeta{ - Status: re.DisabledStatus, - Offset: 0, - Limit: 200, - }, - count: 50, - err: nil, - }, - { - desc: "list by tag", - pm: re.PageMeta{ - Tag: "tag1", - Offset: 0, - Limit: 200, - Status: re.AllStatus, - }, - count: 10, - err: nil, - }, - { - desc: "list with zero limit returns all", - pm: re.PageMeta{ - Status: re.AllStatus, - }, - count: 100, - err: nil, - }, - { - desc: "list non-existing domain", - pm: re.PageMeta{ - Domain: generateUUID(t), - Offset: 0, - Limit: 10, - Status: re.AllStatus, - }, - count: 0, - err: nil, - }, - { - desc: "list ordered by name ascending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: nameOrder, - Dir: ascDir, - }, - count: 10, - err: nil, - }, - { - desc: "list ordered by name descending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: nameOrder, - Dir: descDir, - }, - count: 10, - err: nil, - }, - { - desc: "list ordered by created_at ascending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: createdAtOrder, - Dir: ascDir, - }, - count: 10, - err: nil, - }, - { - desc: "list ordered by created_at descending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: createdAtOrder, - Dir: descDir, - }, - count: 10, - err: nil, - }, - { - desc: "list ordered by updated_at ascending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: updatedAtOrder, - Dir: ascDir, - }, - count: 10, - err: nil, - }, - { - desc: "list ordered by updated_at descending", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - Order: updatedAtOrder, - Dir: descDir, - }, - count: 10, - err: nil, - }, - { - desc: "list with default order (updated_at desc)", - pm: re.PageMeta{ - Offset: 0, - Limit: 10, - Status: re.AllStatus, - }, - count: 10, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - page, err := repo.ListAllRules(context.Background(), tc.pm) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - assert.Equal(t, tc.count, len(page.Rules), fmt.Sprintf("%s: expected %d rules, got %d", tc.desc, tc.count, len(page.Rules))) - if len(page.Rules) > 1 { - switch tc.pm.Order { - case nameOrder: - if tc.pm.Dir == ascDir { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].Name <= page.Rules[j].Name - }), "Expected names to be sorted ascending") - } else { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].Name >= page.Rules[j].Name - }), "Expected names to be sorted descending") - } - case createdAtOrder: - if tc.pm.Dir == ascDir { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].CreatedAt.Before(page.Rules[j].CreatedAt) - }), "Expected created_at to be sorted ascending") - } else { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].CreatedAt.After(page.Rules[j].CreatedAt) - }), "Expected created_at to be sorted descending") - } - case updatedAtOrder: - if tc.pm.Dir == ascDir { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].UpdatedAt.Before(page.Rules[j].UpdatedAt) - }), "Expected updated_at to be sorted ascending") - } else { - assert.True(t, sort.SliceIsSorted(page.Rules, func(i, j int) bool { - return page.Rules[i].UpdatedAt.After(page.Rules[j].UpdatedAt) - }), "Expected updated_at to be sorted descending") - } - } - } - }) - } -} - -func TestRemoveRule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM rules") - assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - rule := re.Rule{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - InputChannel: generateUUID(t), - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - Status: re.EnabledStatus, - CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - UpdatedBy: generateUUID(t), - } - rule, err := repo.AddRule(context.Background(), rule) - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "remove existing rule", - id: rule.ID, - err: nil, - }, - { - desc: "remove non-existing rule", - id: generateUUID(t), - err: repoerr.ErrNotFound, - }, - { - desc: "remove already removed rule", - id: rule.ID, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.RemoveRule(context.Background(), tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - }) - } -} - -func generateUUID(t *testing.T) string { - ulid, err := idProvider.ID() - assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - return ulid -} diff --git a/re/postgres/rule.go b/re/postgres/rule.go deleted file mode 100644 index 0dfc94230..000000000 --- a/re/postgres/rule.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "database/sql" - "encoding/json" - "time" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/re" - "github.com/jackc/pgtype" -) - -// dbRule represents the database structure for a Rule. -type dbRule struct { - ID string `db:"id"` - Name string `db:"name"` - DomainID string `db:"domain_id"` - Tags pgtype.TextArray `db:"tags,omitempty"` - Metadata []byte `db:"metadata,omitempty"` - InputChannel string `db:"input_channel"` - InputTopic sql.NullString `db:"input_topic"` - LogicType re.ScriptType `db:"logic_type"` - LogicValue string `db:"logic_value"` - Outputs []byte `db:"outputs"` - StartDateTime sql.NullTime `db:"start_datetime"` - Time sql.NullTime `db:"time"` - Recurring schedule.Recurring `db:"recurring"` - RecurringPeriod uint `db:"recurring_period"` - Status re.Status `db:"status"` - CreatedAt time.Time `db:"created_at"` - CreatedBy string `db:"created_by"` - UpdatedAt time.Time `db:"updated_at"` - UpdatedBy string `db:"updated_by"` -} - -func ruleToDb(r re.Rule) (dbRule, error) { - metadata := []byte("{}") - if len(r.Metadata) > 0 { - b, err := json.Marshal(r.Metadata) - if err != nil { - return dbRule{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - metadata = b - } - - start := sql.NullTime{Time: r.Schedule.StartDateTime} - if !r.Schedule.StartDateTime.IsZero() { - start.Valid = true - } - t := sql.NullTime{Time: r.Schedule.Time} - if !r.Schedule.Time.IsZero() { - t.Valid = true - } - var tags pgtype.TextArray - if err := tags.Set(r.Tags); err != nil { - return dbRule{}, err - } - - outputs, err := json.Marshal(r.Outputs) - if err != nil { - return dbRule{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - - return dbRule{ - ID: r.ID, - Name: r.Name, - DomainID: r.DomainID, - Tags: tags, - Metadata: metadata, - InputChannel: r.InputChannel, - InputTopic: toNullString(r.InputTopic), - LogicType: r.Logic.Type, - LogicValue: r.Logic.Value, - Outputs: outputs, - StartDateTime: start, - Time: t, - Recurring: r.Schedule.Recurring, - RecurringPeriod: r.Schedule.RecurringPeriod, - Status: r.Status, - CreatedAt: r.CreatedAt, - CreatedBy: r.CreatedBy, - UpdatedAt: r.UpdatedAt, - UpdatedBy: r.UpdatedBy, - }, nil -} - -func dbToRule(dto dbRule) (re.Rule, error) { - var metadata re.Metadata - if dto.Metadata != nil { - if err := json.Unmarshal(dto.Metadata, &metadata); err != nil { - return re.Rule{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - var tags []string - for _, e := range dto.Tags.Elements { - tags = append(tags, e.String) - } - - var outputs re.Outputs - if dto.Outputs != nil { - if err := json.Unmarshal(dto.Outputs, &outputs); err != nil { - return re.Rule{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - return re.Rule{ - ID: dto.ID, - Name: dto.Name, - DomainID: dto.DomainID, - Tags: tags, - Metadata: metadata, - InputChannel: dto.InputChannel, - InputTopic: fromNullString(dto.InputTopic), - Logic: re.Script{ - Type: dto.LogicType, - Value: dto.LogicValue, - }, - Outputs: outputs, - Schedule: schedule.Schedule{ - StartDateTime: dto.StartDateTime.Time, - Time: dto.Time.Time, - Recurring: dto.Recurring, - RecurringPeriod: dto.RecurringPeriod, - }, - Status: dto.Status, - CreatedAt: dto.CreatedAt, - CreatedBy: dto.CreatedBy, - UpdatedAt: dto.UpdatedAt, - UpdatedBy: dto.UpdatedBy, - }, nil -} - -func toNullString(value string) sql.NullString { - if value == "" { - return sql.NullString{Valid: false} - } - return sql.NullString{String: value, Valid: true} -} - -func fromNullString(nullString sql.NullString) string { - if !nullString.Valid { - return "" - } - return nullString.String -} diff --git a/re/postgres/setup_test.go b/re/postgres/setup_test.go deleted file mode 100644 index 162b0b06c..000000000 --- a/re/postgres/setup_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "database/sql" - "fmt" - "log" - "os" - "testing" - "time" - - "github.com/absmach/magistrala/pkg/postgres" - repostgres "github.com/absmach/magistrala/re/postgres" - "github.com/jmoiron/sqlx" - dockertest "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "go.opentelemetry.io/otel" -) - -var ( - db *sqlx.DB - database postgres.Database - tracer = otel.Tracer("repo_tests") -) - -func TestMain(m *testing.M) { - pool, err := dockertest.NewPool("") - if err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - container, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "postgres", - Tag: "16.2-alpine", - Env: []string{ - "POSTGRES_USER=test", - "POSTGRES_PASSWORD=test", - "POSTGRES_DB=test", - "listen_addresses = '*'", - }, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - if err != nil { - log.Fatalf("Could not start container: %s", err) - } - - port := container.GetPort("5432/tcp") - - // exponential backoff-retry, because the application in the container might not be ready to accept connections yet - pool.MaxWait = 120 * time.Second - if err := pool.Retry(func() error { - url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port) - db, err := sql.Open("pgx", url) - if err != nil { - return err - } - return db.Ping() - }); err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - dbConfig := postgres.Config{ - Host: "localhost", - Port: port, - User: "test", - Pass: "test", - Name: "test", - SSLMode: "disable", - SSLCert: "", - SSLKey: "", - SSLRootCert: "", - } - - migration, err := repostgres.Migration() - if err != nil { - log.Fatalf("Could not get migration: %s", err) - } - if db, err = postgres.Setup(dbConfig, *migration); err != nil { - log.Fatalf("Could not setup test DB connection: %s", err) - } - - database = postgres.NewDatabase(db, dbConfig, tracer) - - code := m.Run() - - // Defers will not be run when using os.Exit - db.Close() - if err := pool.Purge(container); err != nil { - log.Fatalf("Could not purge container: %s", err) - } - - os.Exit(code) -} diff --git a/re/rule.go b/re/rule.go deleted file mode 100644 index 8a5e3af5e..000000000 --- a/re/rule.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - "encoding/json" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/re/outputs" -) - -const ( - LuaType ScriptType = iota - GoType -) - -const TimeLayout = "2006-01-02T15:04:05.999999Z" - -type ( - // ScriptType indicates Runtime type for the future versions - // that will support JS or Go runtimes alongside Lua. - ScriptType uint - - Metadata map[string]any - Script struct { - Type ScriptType `json:"type"` - Value string `json:"value"` - } -) - -var outputRegistry = map[outputs.OutputType]func() Runnable{ - outputs.AlarmsType: func() Runnable { return &outputs.Alarm{} }, - outputs.EmailType: func() Runnable { return &outputs.Email{} }, - outputs.SaveRemotePgType: func() Runnable { return &outputs.Postgres{} }, - outputs.ChannelsType: func() Runnable { return &outputs.ChannelPublisher{} }, - outputs.SaveSenMLType: func() Runnable { return &outputs.SenML{} }, - outputs.SlackType: func() Runnable { return &outputs.Slack{} }, -} - -type Rule struct { - ID string `json:"id"` - Name string `json:"name"` - DomainID string `json:"domain"` - Metadata Metadata `json:"metadata,omitempty"` - Tags []string `json:"tags,omitempty"` - InputChannel string `json:"input_channel"` - InputTopic string `json:"input_topic"` - Logic Script `json:"logic"` - Outputs Outputs `json:"outputs,omitempty"` - Schedule schedule.Schedule `json:"schedule,omitempty"` - Status Status `json:"status"` - CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by"` - UpdatedAt time.Time `json:"updated_at"` - UpdatedBy string `json:"updated_by"` -} - -// EventEncode converts a Rule struct to map[string]any at event producer. -func (r Rule) EventEncode() (map[string]any, error) { - m := map[string]any{ - "id": r.ID, - "name": r.Name, - "created_at": r.CreatedAt.Format(TimeLayout), - "created_by": r.CreatedBy, - "schedule": r.Schedule.EventEncode(), - "status": r.Status.String(), - } - - if r.Name != "" { - m["name"] = r.Name - } - - if r.DomainID != "" { - m["domain"] = r.DomainID - } - - if !r.UpdatedAt.IsZero() { - m["updated_at"] = r.UpdatedAt.Format(TimeLayout) - } - - if r.UpdatedBy != "" { - m["updated_by"] = r.UpdatedBy - } - - if len(r.Metadata) > 0 { - m["metadata"] = r.Metadata - } - - if len(r.Tags) > 0 { - m["tags"] = r.Tags - } - - if r.InputChannel != "" { - m["input_channel"] = r.InputChannel - } - - if r.InputTopic != "" { - m["input_topic"] = r.InputTopic - } - - if r.Logic.Value != "" { - m["logic"] = map[string]any{ - "type": r.Logic.Type, - "value": r.Logic.Value, - } - } - - return m, nil -} - -type Outputs []Runnable - -func (o *Outputs) UnmarshalJSON(data []byte) error { - var rawList []json.RawMessage - if err := json.Unmarshal(data, &rawList); err != nil { - return err - } - - var runnables []Runnable - for _, raw := range rawList { - var meta struct { - Type outputs.OutputType `json:"type"` - } - if err := json.Unmarshal(raw, &meta); err != nil { - return err - } - - factory, ok := outputRegistry[meta.Type] - if !ok { - return errors.New("unknown output type: " + meta.Type.String()) - } - - instance := factory() - if err := json.Unmarshal(raw, instance); err != nil { - return err - } - - runnables = append(runnables, instance) - } - v := Outputs(runnables) - *o = v - return nil -} - -type Runnable interface { - Run(ctx context.Context, msg *messaging.Message, val any) error -} - -// PageMeta contains page metadata that helps navigation. -type PageMeta struct { - Total uint64 `json:"total" db:"total"` - Offset uint64 `json:"offset" db:"offset"` - Limit uint64 `json:"limit" db:"limit"` - Dir string `json:"dir" db:"dir"` - Order string `json:"order" db:"order"` - Name string `json:"name" db:"name"` - InputChannel string `json:"input_channel,omitempty" db:"input_channel"` - InputTopic *string `json:"input_topic,omitempty" db:"input_topic"` - Scheduled *bool `json:"scheduled,omitempty"` - OutputChannel string `json:"output_channel,omitempty" db:"output_channel"` - Status Status `json:"status,omitempty" db:"status"` - Domain string `json:"domain_id,omitempty" db:"domain_id"` - Tag string `json:"tag,omitempty"` - ScheduledBefore *time.Time `json:"scheduled_before,omitempty" db:"scheduled_before"` // Filter rules scheduled before this time - ScheduledAfter *time.Time `json:"scheduled_after,omitempty" db:"scheduled_after"` // Filter rules scheduled after this time - Recurring *schedule.Recurring `json:"recurring,omitempty" db:"recurring"` // Filter by recurring type - UserID string `json:"user_id,omitempty" db:"user_id"` -} - -// EventEncode converts a PageMeta struct to map[string]any. -func (pm PageMeta) EventEncode() map[string]any { - m := map[string]any{ - "total": pm.Total, - "offset": pm.Offset, - "limit": pm.Limit, - "status": pm.Status.String(), - "domain_id": pm.Domain, - } - - if pm.Dir != "" { - m["dir"] = pm.Dir - } - if pm.Name != "" { - m["name"] = pm.Name - } - if pm.InputChannel != "" { - m["input_channel"] = pm.InputChannel - } - if pm.InputTopic != nil { - m["input_topic"] = *pm.InputTopic - } - if pm.Scheduled != nil { - m["scheduled"] = *pm.Scheduled - } - if pm.OutputChannel != "" { - m["output_channel"] = pm.OutputChannel - } - if pm.Tag != "" { - m["tag"] = pm.Tag - } - if pm.ScheduledBefore != nil { - m["scheduled_before"] = pm.ScheduledBefore.Format(time.RFC3339Nano) - } - if pm.ScheduledAfter != nil { - m["scheduled_after"] = pm.ScheduledAfter.Format(time.RFC3339Nano) - } - if pm.Recurring != nil { - m["recurring"] = pm.Recurring.String() - } - - return m -} - -type Page struct { - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Total uint64 `json:"total"` - Rules []Rule `json:"rules"` -} - -type Service interface { - messaging.MessageHandler - AddRule(ctx context.Context, session authn.Session, r Rule) (Rule, error) - ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (Rule, error) - UpdateRule(ctx context.Context, session authn.Session, r Rule) (Rule, error) - UpdateRuleTags(ctx context.Context, session authn.Session, r Rule) (Rule, error) - UpdateRuleSchedule(ctx context.Context, session authn.Session, r Rule) (Rule, error) - ListRules(ctx context.Context, session authn.Session, pm PageMeta) (Page, error) - RemoveRule(ctx context.Context, session authn.Session, id string) error - EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error) - DisableRule(ctx context.Context, session authn.Session, id string) (Rule, error) - - StartScheduler(ctx context.Context) error -} - -type Repository interface { - AddRule(ctx context.Context, r Rule) (Rule, error) - ViewRule(ctx context.Context, id string) (Rule, error) - UpdateRule(ctx context.Context, r Rule) (Rule, error) - UpdateRuleTags(ctx context.Context, r Rule) (Rule, error) - UpdateRuleSchedule(ctx context.Context, r Rule) (Rule, error) - RemoveRule(ctx context.Context, id string) error - UpdateRuleStatus(ctx context.Context, r Rule) (Rule, error) - ListAllRules(ctx context.Context, pm PageMeta) (Page, error) - UpdateRuleDue(ctx context.Context, id string, due time.Time) (Rule, error) -} diff --git a/re/service.go b/re/service.go deleted file mode 100644 index a7544e610..000000000 --- a/re/service.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "context" - "time" - - "github.com/absmach/magistrala" - grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/emailer" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - "github.com/absmach/magistrala/pkg/ticker" -) - -var ( - ErrGoroutinesNotAllowed = errors.New("goroutines are not allowed in Go scripts") - ErrPanicNotAllowed = errors.New("panic is not allowed in Go scripts") -) - -type re struct { - repo Repository - runInfo chan pkglog.RunInfo - idp magistrala.IDProvider - rePubSub messaging.PubSub - writersPub messaging.Publisher - alarmsPub messaging.Publisher - ticker ticker.Ticker - email emailer.Emailer - readers grpcReadersV1.ReadersServiceClient -} - -func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp magistrala.IDProvider, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient) (Service, error) { - return &re{ - repo: repo, - idp: idp, - runInfo: runInfo, - rePubSub: rePubSub, - writersPub: writersPub, - alarmsPub: alarmsPub, - ticker: tck, - email: emailer, - readers: readers, - }, nil -} - -func (re *re) AddRule(ctx context.Context, session authn.Session, r Rule) (retRule Rule, retErr error) { - if r.Logic.Type == GoType && goKeywordRegex.MatchString(r.Logic.Value) { - return Rule{}, errors.Wrap(svcerr.ErrMalformedEntity, ErrGoroutinesNotAllowed) - } - if r.Logic.Type == GoType && panicRegex.MatchString(r.Logic.Value) { - return Rule{}, errors.Wrap(svcerr.ErrMalformedEntity, ErrPanicNotAllowed) - } - - id, err := re.idp.ID() - if err != nil { - return Rule{}, err - } - now := time.Now().UTC() - r.CreatedAt = now - r.ID = id - r.CreatedBy = session.UserID - r.DomainID = session.DomainID - r.Status = EnabledStatus - - if !r.Schedule.StartDateTime.IsZero() { - r.Schedule.StartDateTime = now - } - r.Schedule.Time = r.Schedule.StartDateTime - - rule, err := re.repo.AddRule(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - defer func() { - if retErr != nil { - if errRollBack := re.repo.RemoveRule(ctx, rule.ID); errRollBack != nil { - retErr = errors.Wrap(retErr, errors.Wrap(svcerr.ErrRollbackRepo, errRollBack)) - } - } - }() - - return rule, nil -} - -func (re *re) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (Rule, error) { - rule, err := re.repo.ViewRule(ctx, id) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - return rule, nil -} - -func (re *re) UpdateRule(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - if r.Logic.Type == GoType && goKeywordRegex.MatchString(r.Logic.Value) { - return Rule{}, errors.Wrap(svcerr.ErrMalformedEntity, ErrGoroutinesNotAllowed) - } - if r.Logic.Type == GoType && panicRegex.MatchString(r.Logic.Value) { - return Rule{}, errors.Wrap(svcerr.ErrMalformedEntity, ErrPanicNotAllowed) - } - - r.UpdatedAt = time.Now().UTC() - r.UpdatedBy = session.UserID - rule, err := re.repo.UpdateRule(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return rule, nil -} - -func (re *re) UpdateRuleTags(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - r.UpdatedAt = time.Now().UTC() - r.UpdatedBy = session.UserID - rule, err := re.repo.UpdateRuleTags(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return rule, nil -} - -func (re *re) UpdateRuleSchedule(ctx context.Context, session authn.Session, r Rule) (Rule, error) { - r.UpdatedAt = time.Now().UTC() - r.UpdatedBy = session.UserID - rule, err := re.repo.UpdateRuleSchedule(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return rule, nil -} - -func (re *re) ListRules(ctx context.Context, session authn.Session, pm PageMeta) (Page, error) { - pm.Domain = session.DomainID - page, err := re.repo.ListAllRules(ctx, pm) - if err != nil { - return Page{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - return page, nil -} - -func (re *re) RemoveRule(ctx context.Context, session authn.Session, id string) error { - if err := re.repo.RemoveRule(ctx, id); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - return nil -} - -func (re *re) EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error) { - status, err := ToStatus(Enabled) - if err != nil { - return Rule{}, err - } - r := Rule{ - ID: id, - UpdatedAt: time.Now().UTC(), - UpdatedBy: session.UserID, - Status: status, - } - rule, err := re.repo.UpdateRuleStatus(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return rule, nil -} - -func (re *re) DisableRule(ctx context.Context, session authn.Session, id string) (Rule, error) { - status, err := ToStatus(Disabled) - if err != nil { - return Rule{}, err - } - r := Rule{ - ID: id, - UpdatedAt: time.Now().UTC(), - UpdatedBy: session.UserID, - Status: status, - } - rule, err := re.repo.UpdateRuleStatus(ctx, r) - if err != nil { - return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return rule, nil -} - -func (re *re) Cancel() error { - return nil -} diff --git a/re/service_test.go b/re/service_test.go deleted file mode 100644 index 0cf77b607..000000000 --- a/re/service_test.go +++ /dev/null @@ -1,1822 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re_test - -import ( - "context" - "fmt" - "log/slog" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/authn" - emocks "github.com/absmach/magistrala/pkg/emailer/mocks" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/messaging" - pubsubmocks "github.com/absmach/magistrala/pkg/messaging/mocks" - policymocks "github.com/absmach/magistrala/pkg/policies/mocks" - pkgSch "github.com/absmach/magistrala/pkg/schedule" - tmocks "github.com/absmach/magistrala/pkg/ticker/mocks" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/absmach/magistrala/re" - "github.com/absmach/magistrala/re/mocks" - "github.com/absmach/magistrala/re/outputs" - readmocks "github.com/absmach/magistrala/readers/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -// unknownOutput is a mock output type that doesn't match any known output type. -type unknownOutput struct{} - -func (u *unknownOutput) Run(ctx context.Context, msg *messaging.Message, val any) error { - return nil -} - -func (u *unknownOutput) MarshalJSON() ([]byte, error) { - return []byte(`{"type": "unknown"}`), nil -} - -var ( - namegen = namegenerator.NewGenerator() - userID = testsutil.GenerateUUID(&testing.T{}) - domainID = testsutil.GenerateUUID(&testing.T{}) - ruleName = namegen.Generate() - ruleID = testsutil.GenerateUUID(&testing.T{}) - Tags = []string{"tag1", "tag2"} - inputChannel = "test.channel" - StartDateTime = time.Now().Add(-time.Hour) - schedule = pkgSch.Schedule{ - StartDateTime: StartDateTime, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: time.Now().Add(-time.Hour), - } -) - -func newService(t *testing.T, runInfo chan pkglog.RunInfo) (re.Service, *mocks.Repository, *pubsubmocks.PubSub, *tmocks.Ticker, *emocks.Emailer, *policymocks.Service) { - repo := new(mocks.Repository) - mockTicker := new(tmocks.Ticker) - idProvider := uuid.NewMock() - pubsub := pubsubmocks.NewPubSub(t) - readersSvc := new(readmocks.ReadersServiceClient) - e := new(emocks.Emailer) - policy := new(policymocks.Service) - svc, err := re.NewService(repo, runInfo, idProvider, pubsub, pubsub, pubsub, mockTicker, e, readersSvc) - if err != nil { - t.Fatalf("Failed to create service: %v", err) - } - return svc, repo, pubsub, mockTicker, e, policy -} - -func TestAddRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - ruleName := namegen.Generate() - now := time.Now().Add(time.Hour) - cases := []struct { - desc string - session authn.Session - rule re.Rule - res re.Rule - err error - }{ - { - desc: "Add rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - }, - res: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: nil, - }, - { - desc: "Add rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "Add rule with non-zero StartDateTime", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - StartDateTime: now, - Recurring: pkgSch.Weekly, - RecurringPeriod: 2, - Time: now.Add(2 * time.Hour), - }, - }, - res: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - StartDateTime: now, - Recurring: pkgSch.Weekly, - RecurringPeriod: 2, - Time: now.Add(2 * time.Hour), - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: nil, - }, - { - desc: "Add rule with Go script containing goroutines", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Logic: re.Script{ - Type: re.GoType, - Value: `func logicFunction() any { go func() {}(); return true }`, - }, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - }, - err: re.ErrGoroutinesNotAllowed, - }, - { - desc: "Add rule with Go script containing panic", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Logic: re.Script{ - Type: re.GoType, - Value: `func logicFunction() any { panic("error"); return true }`, - }, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - }, - err: re.ErrPanicNotAllowed, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("AddRule", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.AddRule(context.Background(), tc.session, tc.rule) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.NotEmpty(t, res.ID, "expected non-empty result in ID") - assert.Equal(t, tc.rule.Name, res.Name) - assert.Equal(t, tc.rule.Schedule, res.Schedule) - } - repoCall.Unset() - }) - } -} - -func TestAddRuleWithoutRoleProvisioning(t *testing.T) { - repo := new(mocks.Repository) - mockTicker := new(tmocks.Ticker) - idProvider := uuid.NewMock() - pubsub := pubsubmocks.NewPubSub(t) - readersSvc := new(readmocks.ReadersServiceClient) - e := new(emocks.Emailer) - - svc, err := re.NewService(repo, make(chan pkglog.RunInfo), idProvider, pubsub, pubsub, pubsub, mockTicker, e, readersSvc) - if err != nil { - t.Fatalf("Failed to create service: %v", err) - } - - session := authn.Session{ - UserID: userID, - DomainID: domainID, - } - rule := re.Rule{ - Name: ruleName, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: time.Now().Add(time.Hour), - }, - } - saved := rule - saved.ID = ruleID - saved.Status = re.EnabledStatus - saved.CreatedBy = userID - saved.DomainID = domainID - - repo.On("AddRule", mock.Anything, mock.Anything).Return(saved, nil).Once() - - res, err := svc.AddRule(context.Background(), session, rule) - assert.NoError(t, err) - assert.Equal(t, saved.ID, res.ID) - repo.AssertNotCalled(t, "AddRoles", mock.Anything, mock.Anything) - repo.AssertExpectations(t) -} - -func TestViewRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - now := time.Now().Add(time.Hour) - cases := []struct { - desc string - session authn.Session - id string - res re.Rule - err error - }{ - { - desc: "view rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - res: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: nil, - }, - { - desc: "view rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("ViewRule", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.ViewRule(context.Background(), tc.session, tc.id, false) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestUpdateRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - newName := namegen.Generate() - now := time.Now().Add(time.Hour) - cases := []struct { - desc string - session authn.Session - rule re.Rule - res re.Rule - err error - }{ - { - desc: "update rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: newName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - res: re.Rule{ - Name: newName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - UpdatedAt: now, - UpdatedBy: userID, - }, - err: nil, - }, - { - desc: "update rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: svcerr.ErrUpdateEntity, - }, - { - desc: "update rule with Go script containing goroutines", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Logic: re.Script{ - Type: re.GoType, - Value: `func logicFunction() any { go processData(); return true }`, - }, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: re.ErrGoroutinesNotAllowed, - }, - { - desc: "Update rule with Go script containing panic", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - rule: re.Rule{ - Name: ruleName, - ID: ruleID, - InputChannel: inputChannel, - Logic: re.Script{ - Type: re.GoType, - Value: `func logicFunction() any { panic("test panic"); return true }`, - }, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - }, - Status: re.EnabledStatus, - CreatedBy: userID, - DomainID: domainID, - }, - err: re.ErrPanicNotAllowed, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateRule", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.UpdateRule(context.Background(), tc.session, tc.rule) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestUpdateRuleTags(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - updateReq re.Rule - repoResp re.Rule - repoErr error - err error - }{ - { - desc: "update rule tags successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - updateReq: re.Rule{ - ID: testsutil.GenerateUUID(t), - Tags: []string{"tag1", "tag2"}, - }, - repoResp: re.Rule{ - ID: testsutil.GenerateUUID(t), - Tags: []string{"tag1", "tag2"}, - }, - }, - { - desc: "update rule tags with repo error", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - updateReq: re.Rule{ - ID: testsutil.GenerateUUID(t), - Tags: []string{"tag1", "tag2"}, - }, - repoErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateRuleTags", context.Background(), mock.Anything).Return(tc.repoResp, tc.repoErr) - got, err := svc.UpdateRuleTags(context.Background(), tc.session, tc.updateReq) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.repoResp, got) - ok := repo.AssertCalled(t, "UpdateRuleTags", context.Background(), mock.Anything) - assert.True(t, ok, fmt.Sprintf("UpdateTags was not called on %s", tc.desc)) - } - repoCall.Unset() - }) - } -} - -func TestUpdateRuleSchedule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - now := time.Now().UTC() - future := now.Add(2 * time.Hour) - newSchedule := pkgSch.Schedule{ - StartDateTime: future, - Time: future.Add(time.Hour), - Recurring: pkgSch.Weekly, - RecurringPeriod: 2, - } - - cases := []struct { - desc string - session authn.Session - updateReq re.Rule - repoResp re.Rule - repoErr error - err error - }{ - { - desc: "update rule schedule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - updateReq: re.Rule{ - ID: testsutil.GenerateUUID(t), - Schedule: newSchedule, - }, - repoResp: re.Rule{ - ID: testsutil.GenerateUUID(t), - Schedule: newSchedule, - UpdatedAt: now, - UpdatedBy: userID, - }, - }, - { - desc: "update rule schedule with repo error", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - updateReq: re.Rule{ - ID: testsutil.GenerateUUID(t), - Schedule: newSchedule, - }, - repoErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateRuleSchedule", context.Background(), mock.Anything).Return(tc.repoResp, tc.repoErr) - got, err := svc.UpdateRuleSchedule(context.Background(), tc.session, tc.updateReq) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.repoResp, got) - ok := repo.AssertCalled(t, "UpdateRuleSchedule", context.Background(), mock.Anything) - assert.True(t, ok, fmt.Sprintf("UpdateRuleSchedule was not called on %s", tc.desc)) - } - repoCall.Unset() - }) - } -} - -func TestListRules(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - numRules := 50 - now := time.Now().Add(time.Hour) - var rules []re.Rule - for i := 0; i < numRules; i++ { - r := re.Rule{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: re.EnabledStatus, - CreatedAt: now, - CreatedBy: userID, - Schedule: pkgSch.Schedule{ - Recurring: pkgSch.Daily, - Time: now.Add(1 * time.Hour), - RecurringPeriod: 1, - StartDateTime: now, - }, - } - rules = append(rules, r) - } - - goRule := re.Rule{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: re.EnabledStatus, - CreatedAt: now, - CreatedBy: userID, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - } - - cases := []struct { - desc string - session authn.Session - pageMeta re.PageMeta - res re.Page - err error - superAdmin bool - }{ - { - desc: "list rules successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{}, - res: re.Page{ - Total: uint64(numRules), - Offset: 0, - Limit: 10, - Rules: rules[0:10], - }, - err: nil, - }, - { - desc: "list rules with go type", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{}, - res: re.Page{ - Total: 1, - Offset: 0, - Limit: 10, - Rules: []re.Rule{goRule}, - }, - err: nil, - }, - { - desc: "list rules successfully with limit", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{ - Limit: 100, - }, - res: re.Page{ - Total: uint64(numRules), - Offset: 0, - Limit: 100, - Rules: rules[0:numRules], - }, - err: nil, - }, - { - desc: "list rules successfully with offset", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{ - Offset: 20, - Limit: 10, - }, - res: re.Page{ - Total: uint64(numRules), - Offset: 20, - Limit: 10, - Rules: rules[20:30], - }, - err: nil, - }, - { - desc: "list rules with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{}, - err: svcerr.ErrViewEntity, - }, - { - desc: "list rules as super admin successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - SuperAdmin: true, - }, - pageMeta: re.PageMeta{}, - res: re.Page{ - Total: uint64(numRules), - Offset: 0, - Limit: 10, - Rules: rules[0:10], - }, - superAdmin: true, - err: nil, - }, - { - desc: "list rules as super admin with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - SuperAdmin: true, - }, - pageMeta: re.PageMeta{}, - superAdmin: true, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - var repoCall *mock.Call - repoCall = repo.On("ListAllRules", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.ListRules(context.Background(), tc.session, tc.pageMeta) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestRemoveRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, policies := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - err error - deletePoliciesErr error - }{ - { - desc: "remove rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - err: nil, - deletePoliciesErr: nil, - }, - { - desc: "remove rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - err: svcerr.ErrRemoveEntity, - deletePoliciesErr: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RemoveRule", mock.Anything, mock.Anything).Return(tc.err) - policyCall := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePoliciesErr) - err := svc.RemoveRule(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - policyCall.Unset() - repoCall.Unset() - }) - } -} - -func TestEnableRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - now := time.Now() - - cases := []struct { - desc string - session authn.Session - id string - status re.Status - res re.Rule - err error - }{ - { - desc: "enable rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - status: re.EnabledStatus, - res: re.Rule{ - ID: ruleID, - Name: ruleName, - DomainID: domainID, - InputChannel: inputChannel, - Status: re.EnabledStatus, - Schedule: schedule, - UpdatedBy: userID, - UpdatedAt: now, - }, - err: nil, - }, - { - desc: "enable rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - status: re.EnabledStatus, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateRuleStatus", context.Background(), mock.Anything).Return(tc.res, tc.err) - res, err := svc.EnableRule(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestDisableRule(t *testing.T) { - // nolint:dogsled - svc, repo, _, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - now := time.Now() - - cases := []struct { - desc string - session authn.Session - id string - status re.Status - res re.Rule - err error - }{ - { - desc: "disable rule successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - status: re.DisabledStatus, - res: re.Rule{ - ID: ruleID, - Name: ruleName, - DomainID: domainID, - InputChannel: inputChannel, - Status: re.DisabledStatus, - Schedule: schedule, - UpdatedBy: userID, - UpdatedAt: now, - }, - err: nil, - }, - { - desc: "disable rule with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: ruleID, - status: re.DisabledStatus, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateRuleStatus", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.DisableRule(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestHandle(t *testing.T) { - svc, repo, pubmocks, _, emailer, _ := newService(t, make(chan pkglog.RunInfo)) - now := time.Now() - scheduled := false - - cases := []struct { - desc string - message *messaging.Message - page re.Page - listErr error - publishErr error - expectErr bool - }{ - { - desc: "consume message with empty rules", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{}, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script returning true", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: "return message.payload", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script returning false", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: "return false", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script with no outputs", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: "return message.payload", - }, - Outputs: re.Outputs{}, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script returning nil", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: "return nil", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script with invalid syntax", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: "invalid lua syntax {{{", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and Alarm output", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 30.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return {severity = 2, description = "High temperature"}`, - }, - Outputs: re.Outputs{ - &outputs.Alarm{ - RuleID: testsutil.GenerateUUID(t), - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and SenML output", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return {bn = "sensor1", n = "temperature", v = 25.5}`, - }, - Outputs: re.Outputs{ - &outputs.SenML{}, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and Email output", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return message.payload`, - }, - Outputs: re.Outputs{ - &outputs.Email{ - To: []string{"test@example.com"}, - Subject: "Temperature Alert", - Content: "Temperature: {{.Result}}", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with rules using GoType", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType logic returning false", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return false }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType invalid logic value", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "invalid go code {{{", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType missing logicFunction", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func someOtherFunc() bool { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType invalid function signature", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "var logicFunction = 42", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType function logicFunction properly named", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func logicFunction() any { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType returning non-bool", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func() any { return \"not a bool\" }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType and JSON payload", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25, "humidity": 60}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType and invalid JSON payload", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`invalid json {{{`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with GoType script that panics", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"value": 42}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.GoType, - Value: `func logicFunction() any { panic("test") }`, - }, - Outputs: re.Outputs{ - &outputs.ChannelPublisher{ - Channel: "output.channel", - Topic: "output.topic", - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and Postgres output", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5, "humidity": 60}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return message.payload`, - }, - Outputs: re.Outputs{ - &outputs.Postgres{ - Host: "localhost", - Port: 5432, - User: "test", - Password: "test", - Database: "testdb", - Table: "sensor_data", - Mapping: `{"temperature": {{.Result.temperature}}, "humidity": {{.Result.humidity}}}`, - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and Slack output", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return message.payload`, - }, - Outputs: re.Outputs{ - &outputs.Slack{ - Token: "xoxb-test-token", - ChannelID: "C12345678", - Message: `{"text": "Temperature: {{.Result.temperature}}"}`, - }, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - { - desc: "consume message with Lua script and unknown output type", - message: &messaging.Message{ - Channel: inputChannel, - Created: now.Unix(), - Payload: []byte(`{"temperature": 25.5}`), - }, - page: re.Page{ - Rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - InputChannel: inputChannel, - Status: re.EnabledStatus, - Logic: re.Script{ - Type: re.LuaType, - Value: `return message.payload`, - }, - Outputs: re.Outputs{ - &unknownOutput{}, - }, - Schedule: schedule, - }, - }, - }, - listErr: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - var err error - - repoCall := repo.On("ListAllRules", mock.Anything, re.PageMeta{Domain: tc.message.Domain, InputChannel: tc.message.Channel, Scheduled: &scheduled}).Return(tc.page, tc.listErr).Run(func(args mock.Arguments) { - if tc.listErr != nil { - err = tc.listErr - } - }) - repoCall1 := pubmocks.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(tc.publishErr).Maybe() - repoCall2 := emailer.On("SendEmailNotification", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() - - err = svc.Handle(tc.message) - assert.Nil(t, err) - - time.Sleep(100 * time.Millisecond) - - assert.True(t, errors.Contains(err, tc.listErr), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.listErr, err)) - - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - }) - } -} - -func TestStartScheduler(t *testing.T) { - now := time.Now().Truncate(time.Minute) - ri := make(chan pkglog.RunInfo) - // nolint:dogsled - svc, repo, _, ticker, _, _ := newService(t, ri) - - ctxCases := []struct { - desc string - err error - pageMeta re.PageMeta - page re.Page - listErr error - setupCtx func() (context.Context, context.CancelFunc) - }{ - { - desc: "start scheduler with canceled context", - err: context.Canceled, - pageMeta: re.PageMeta{ - Status: re.EnabledStatus, - ScheduledBefore: &now, - }, - setupCtx: func() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - return ctx, cancel - }, - }, - { - desc: "start scheduler with timeout", - err: context.DeadlineExceeded, - pageMeta: re.PageMeta{ - Status: re.EnabledStatus, - ScheduledBefore: &now, - }, - setupCtx: func() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), time.Millisecond) - }, - }, - { - desc: "start scheduler with deadline exceeded", - err: context.DeadlineExceeded, - pageMeta: re.PageMeta{ - Status: re.EnabledStatus, - ScheduledBefore: &now, - }, - page: re.Page{}, - setupCtx: func() (context.Context, context.CancelFunc) { - return context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond)) - }, - }, - } - - for _, tc := range ctxCases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("ListAllRules", mock.Anything, mock.Anything).Return(tc.page, tc.listErr) - tickChan := make(chan time.Time) - tickCall := ticker.On("Tick").Return((<-chan time.Time)(tickChan)) - tickCall1 := ticker.On("Stop").Return() - ctx, cancel := tc.setupCtx() - defer cancel() - errc := make(chan error) - - go func() { - errc <- svc.StartScheduler(ctx) - }() - - err := <-errc - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v but got %v", tc.err, err)) - repoCall.Unset() - tickCall.Unset() - tickCall1.Unset() - }) - } - - schedulerCases := []struct { - desc string - rules []re.Rule - listErr error - updateDueErr error - expectedRunInfo int - }{ - { - desc: "start scheduler with successful rule processing", - rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - InputChannel: inputChannel, - Status: re.EnabledStatus, - Schedule: pkgSch.Schedule{ - StartDateTime: now.Add(-time.Hour), - Time: now.Add(time.Hour), - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - }, - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - }, - }, - listErr: nil, - updateDueErr: nil, - expectedRunInfo: 1, - }, - { - desc: "start scheduler with multiple rules", - rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - InputChannel: inputChannel, - Status: re.EnabledStatus, - Schedule: pkgSch.Schedule{ - StartDateTime: now.Add(-time.Hour), - Time: now.Add(time.Hour), - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - }, - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - }, - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - InputChannel: inputChannel, - Status: re.EnabledStatus, - Schedule: pkgSch.Schedule{ - StartDateTime: now.Add(-time.Hour), - Time: now.Add(time.Hour), - Recurring: pkgSch.Weekly, - RecurringPeriod: 1, - }, - Logic: re.Script{ - Type: re.GoType, - Value: "func() bool { return true }", - }, - }, - }, - listErr: nil, - updateDueErr: nil, - expectedRunInfo: 2, - }, - { - desc: "start scheduler with list rules error", - rules: []re.Rule{}, - listErr: repoerr.ErrViewEntity, - updateDueErr: nil, - expectedRunInfo: 1, - }, - { - desc: "start scheduler with update due error", - rules: []re.Rule{ - { - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - InputChannel: inputChannel, - Status: re.EnabledStatus, - Schedule: pkgSch.Schedule{ - StartDateTime: now.Add(-time.Hour), - Time: now.Add(time.Hour), - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - }, - Logic: re.Script{ - Type: re.LuaType, - Value: "return true", - }, - }, - }, - listErr: nil, - updateDueErr: repoerr.ErrUpdateEntity, - expectedRunInfo: 1, - }, - } - - for _, tc := range schedulerCases { - t.Run(tc.desc, func(t *testing.T) { - page := re.Page{ - Rules: tc.rules, - Total: uint64(len(tc.rules)), - } - - repoCall := repo.On("ListAllRules", mock.Anything, mock.Anything).Return(page, tc.listErr) - repoCall2 := repo.On("UpdateRuleDue", mock.Anything, mock.Anything, mock.Anything).Return(re.Rule{}, tc.updateDueErr) - tickChan := make(chan time.Time, 1) - tickCall := ticker.On("Tick").Return((<-chan time.Time)(tickChan)) - tickCall1 := ticker.On("Stop").Return() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - go func() { - _ = svc.StartScheduler(ctx) - }() - - tickChan <- now - - collected := 0 - timeout := time.After(500 * time.Millisecond) - for collected < tc.expectedRunInfo { - select { - case info := <-ri: - collected++ - if tc.listErr != nil { - assert.Equal(t, slog.LevelError, info.Level) - assert.Contains(t, info.Message, "failed to list rules") - } else if tc.updateDueErr != nil { - assert.Equal(t, slog.LevelError, info.Level) - assert.Contains(t, info.Message, "failed to update rule") - } else { - assert.True(t, info.Level == slog.LevelInfo || info.Level == slog.LevelWarn || info.Level == slog.LevelError) - } - case <-timeout: - t.Fatalf("timeout waiting for runInfo messages, expected %d got %d", tc.expectedRunInfo, collected) - } - } - - cancel() - time.Sleep(50 * time.Millisecond) - - repoCall.Unset() - repoCall2.Unset() - tickCall.Unset() - tickCall1.Unset() - }) - } -} diff --git a/re/status.go b/re/status.go deleted file mode 100644 index 5042893bf..000000000 --- a/re/status.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -import ( - "encoding/json" - "strings" - - svcerr "github.com/absmach/magistrala/pkg/errors/service" -) - -// Status represents Rule status. -type Status uint8 - -// Possible User status values. -const ( - // EnabledStatus represents enabled Rule. - EnabledStatus Status = iota - // DisabledStatus represents disabled Rule. - DisabledStatus - // DeletedStatus represents a rule that will be deleted. - DeletedStatus - - // AllStatus is used for querying purposes to list rules irrespective - // of their status - both enabled and disabled. It is never stored in the - // database as the actual User status and should always be the largest - // value in this enumeration. - AllStatus -) - -// String representation of the possible status values. -const ( - Disabled = "disabled" - Enabled = "enabled" - Deleted = "deleted" - All = "all" - Unknown = "unknown" -) - -func (s Status) String() string { - switch s { - case DisabledStatus: - return Disabled - case EnabledStatus: - return Enabled - case DeletedStatus: - return Deleted - case AllStatus: - return All - default: - return Unknown - } -} - -// ToStatus converts string value to a valid status. -func ToStatus(status string) (Status, error) { - switch status { - case "", Enabled: - return EnabledStatus, nil - case Disabled: - return DisabledStatus, nil - case Deleted: - return DeletedStatus, nil - case All: - return AllStatus, nil - } - return Status(0), svcerr.ErrInvalidStatus -} - -func (s Status) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -func (s *Status) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToStatus(str) - *s = val - return err -} diff --git a/re/status_test.go b/re/status_test.go deleted file mode 100644 index 9369f9f65..000000000 --- a/re/status_test.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re_test - -import ( - "encoding/json" - "testing" - - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/re" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestToStatus(t *testing.T) { - cases := []struct { - desc string - status string - res re.Status - err error - }{ - { - desc: "convert enabled status", - status: re.Enabled, - res: re.EnabledStatus, - err: nil, - }, - { - desc: "convert empty string to enabled status", - status: "", - res: re.EnabledStatus, - err: nil, - }, - { - desc: "convert disabled status", - status: re.Disabled, - res: re.DisabledStatus, - err: nil, - }, - { - desc: "convert deleted status", - status: re.Deleted, - res: re.DeletedStatus, - err: nil, - }, - { - desc: "convert all status", - status: re.All, - res: re.AllStatus, - err: nil, - }, - { - desc: "convert invalid status", - status: "invalid", - res: re.Status(0), - err: svcerr.ErrInvalidStatus, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - status, err := re.ToStatus(tc.status) - assert.Equal(t, tc.err, err) - assert.Equal(t, tc.res, status) - }) - } -} - -func TestStatusString(t *testing.T) { - cases := []struct { - desc string - status re.Status - res string - }{ - { - desc: "enabled status to string", - status: re.EnabledStatus, - res: re.Enabled, - }, - { - desc: "disabled status to string", - status: re.DisabledStatus, - res: re.Disabled, - }, - { - desc: "deleted status to string", - status: re.DeletedStatus, - res: re.Deleted, - }, - { - desc: "all status to string", - status: re.AllStatus, - res: re.All, - }, - { - desc: "unknown status to string", - status: re.Status(99), - res: re.Unknown, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - assert.Equal(t, tc.res, tc.status.String()) - }) - } -} - -func TestStatusMarshalJSON(t *testing.T) { - cases := []struct { - desc string - status re.Status - res string - }{ - { - desc: "marshal enabled status", - status: re.EnabledStatus, - res: `"enabled"`, - }, - { - desc: "marshal disabled status", - status: re.DisabledStatus, - res: `"disabled"`, - }, - { - desc: "marshal deleted status", - status: re.DeletedStatus, - res: `"deleted"`, - }, - { - desc: "marshal all status", - status: re.AllStatus, - res: `"all"`, - }, - { - desc: "marshal unknown status", - status: re.Status(99), - res: `"unknown"`, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data, err := json.Marshal(tc.status) - require.NoError(t, err) - assert.Equal(t, tc.res, string(data)) - }) - } -} - -func TestStatusUnmarshalJSON(t *testing.T) { - cases := []struct { - desc string - data string - res re.Status - err error - }{ - { - desc: "unmarshal enabled status", - data: `"enabled"`, - res: re.EnabledStatus, - err: nil, - }, - { - desc: "unmarshal disabled status", - data: `"disabled"`, - res: re.DisabledStatus, - err: nil, - }, - { - desc: "unmarshal deleted status", - data: `"deleted"`, - res: re.DeletedStatus, - err: nil, - }, - { - desc: "unmarshal all status", - data: `"all"`, - res: re.AllStatus, - err: nil, - }, - { - desc: "unmarshal empty string to enabled status", - data: `""`, - res: re.EnabledStatus, - err: nil, - }, - { - desc: "unmarshal invalid status", - data: `"invalid"`, - res: re.Status(0), - err: svcerr.ErrInvalidStatus, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - var status re.Status - err := json.Unmarshal([]byte(tc.data), &status) - assert.Equal(t, tc.err, err) - assert.Equal(t, tc.res, status) - }) - } -} diff --git a/readers/api/grpc/client.go b/readers/api/grpc/client.go index 72bbf5728..eb480c1f4 100644 --- a/readers/api/grpc/client.go +++ b/readers/api/grpc/client.go @@ -4,237 +4,16 @@ package grpc import ( - "context" - "encoding/json" - "fmt" - "strings" "time" grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/transformers/senml" - readers "github.com/absmach/magistrala/readers" - "github.com/go-kit/kit/endpoint" - kitgrpc "github.com/go-kit/kit/transport/grpc" + "github.com/absmach/magistrala/pkg/readersclient" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) -const readersSvcName = "readers.v1.ReadersService" - -var _ grpcReadersV1.ReadersServiceClient = (*readersGrpcClient)(nil) - -type readersGrpcClient struct { - readMessages endpoint.Endpoint - timeout time.Duration -} - -// NewReadersClient returns new readers gRPC client instance. +// NewReadersClient returns a reader gRPC client. +// +// Deprecated: use readersclient.New. func NewReadersClient(conn *grpc.ClientConn, timeout time.Duration) grpcReadersV1.ReadersServiceClient { - return &readersGrpcClient{ - readMessages: kitgrpc.NewClient( - conn, - readersSvcName, - "ReadMessages", - encodeReadMessagesRequest, - decodeReadMessagesResponse, - grpcReadersV1.ReadMessagesRes{}, - ).Endpoint(), - timeout: timeout, - } -} - -func (client readersGrpcClient) ReadMessages(ctx context.Context, in *grpcReadersV1.ReadMessagesReq, opts ...grpc.CallOption) (*grpcReadersV1.ReadMessagesRes, error) { - ctx, cancel := context.WithTimeout(ctx, client.timeout) - defer cancel() - - res, err := client.readMessages(ctx, readMessagesReq{ - chanID: in.GetChannelId(), - domain: in.GetDomainId(), - pageMeta: readers.PageMetadata{ - Offset: in.GetPageMetadata().GetOffset(), - Limit: in.GetPageMetadata().GetLimit(), - Comparator: in.GetPageMetadata().GetComparator(), - Aggregation: in.GetPageMetadata().GetAggregation().String(), - From: in.GetPageMetadata().GetFrom(), - To: in.GetPageMetadata().GetTo(), - Interval: in.GetPageMetadata().GetInterval(), - Subtopic: in.GetPageMetadata().GetSubtopic(), - Publisher: in.GetPageMetadata().GetPublisher(), - Publishers: in.GetPageMetadata().GetPublishers(), - Protocol: in.GetPageMetadata().GetProtocol(), - Name: in.GetPageMetadata().GetName(), - Value: in.GetPageMetadata().GetValue(), - BoolValue: in.GetPageMetadata().GetBoolValue(), - StringValue: in.GetPageMetadata().GetStringValue(), - DataValue: in.GetPageMetadata().GetDataValue(), - Format: in.GetPageMetadata().GetFormat(), - }, - }) - if err != nil { - return &grpcReadersV1.ReadMessagesRes{}, decodeError(err) - } - - dpr := res.(readMessagesRes) - return &grpcReadersV1.ReadMessagesRes{ - Total: dpr.Total, - Messages: toResponseMessages(dpr.Messages), - PageMetadata: &grpcReadersV1.PageMetadata{ - Offset: dpr.PageMetadata.Offset, - Limit: dpr.PageMetadata.Limit, - }, - }, nil -} - -func decodeReadMessagesResponse(_ context.Context, grpcRes any) (any, error) { - res := grpcRes.(*grpcReadersV1.ReadMessagesRes) - return readMessagesRes{ - Total: res.Total, - Messages: fromResponseMessages(res.Messages), - PageMetadata: readers.PageMetadata{ - Offset: res.GetPageMetadata().GetOffset(), - Limit: res.GetPageMetadata().GetLimit(), - Order: res.GetPageMetadata().GetOrder(), - Dir: res.GetPageMetadata().GetDir(), - }, - }, nil -} - -func encodeReadMessagesRequest(_ context.Context, grpcReq any) (any, error) { - req := grpcReq.(readMessagesReq) - return &grpcReadersV1.ReadMessagesReq{ - ChannelId: req.chanID, - DomainId: req.domain, - PageMetadata: &grpcReadersV1.PageMetadata{ - Offset: req.pageMeta.Offset, - Limit: req.pageMeta.Limit, - Comparator: req.pageMeta.Comparator, - Aggregation: parseAggregation(req.pageMeta.Aggregation), - From: req.pageMeta.From, - To: req.pageMeta.To, - Interval: req.pageMeta.Interval, - Subtopic: req.pageMeta.Subtopic, - Publisher: req.pageMeta.Publisher, - Publishers: req.pageMeta.Publishers, - Protocol: req.pageMeta.Protocol, - Name: req.pageMeta.Name, - Value: req.pageMeta.Value, - BoolValue: req.pageMeta.BoolValue, - StringValue: req.pageMeta.StringValue, - DataValue: req.pageMeta.DataValue, - Format: req.pageMeta.Format, - Order: req.pageMeta.Order, - Dir: req.pageMeta.Dir, - }, - }, nil -} - -func fromResponseMessages(protoMessages []*grpcReadersV1.Message) []readers.Message { - var messages []readers.Message - for _, m := range protoMessages { - switch msg := m.Payload.(type) { - case *grpcReadersV1.Message_Senml: - s := msg.Senml - base := s.GetBase() - typed := senml.Message{ - Channel: base.GetChannel(), - Subtopic: base.GetSubtopic(), - Publisher: base.GetPublisher(), - Protocol: base.GetProtocol(), - Name: s.GetName(), - Unit: s.GetUnit(), - Time: s.GetTime(), - UpdateTime: s.GetUpdateTime(), - Value: optionalFloat64(s.GetValue()), - StringValue: optionalString(s.GetStringValue()), - DataValue: optionalString(s.GetDataValue()), - BoolValue: optionalBool(s.GetBoolValue()), - Sum: optionalFloat64(s.GetSum()), - } - messages = append(messages, typed) - case *grpcReadersV1.Message_Json: - j := msg.Json - base := j.GetBase() - var p map[string]any - if err := json.Unmarshal(j.GetPayload(), &p); err != nil { - continue - } - messages = append(messages, map[string]any{ - "channel": base.GetChannel(), - "created": j.GetCreated(), - "subtopic": base.GetSubtopic(), - "publisher": base.GetPublisher(), - "protocol": base.GetProtocol(), - "payload": p, - }) - } - } - return messages -} - -func parseAggregation(agg string) grpcReadersV1.Aggregation { - switch strings.ToUpper(agg) { - case aggregationMax: - return grpcReadersV1.Aggregation_AGGREGATION_MAX - case aggregationMin: - return grpcReadersV1.Aggregation_AGGREGATION_MIN - case aggregationSum: - return grpcReadersV1.Aggregation_AGGREGATION_SUM - case aggregationCount: - return grpcReadersV1.Aggregation_AGGREGATION_COUNT - case aggregationAvg: - return grpcReadersV1.Aggregation_AGGREGATION_AVG - default: - return grpcReadersV1.Aggregation_AGGREGATION_UNSPECIFIED - } -} - -func decodeError(err error) error { - if st, ok := status.FromError(err); ok { - switch st.Code() { - case codes.Unauthenticated: - return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message())) - case codes.PermissionDenied: - return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message())) - case codes.InvalidArgument: - return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) - case codes.FailedPrecondition: - return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) - case codes.NotFound: - return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message())) - case codes.AlreadyExists: - return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message())) - case codes.OK: - if msg := st.Message(); msg != "" { - return errors.Wrap(errors.ErrUnidentified, errors.New(msg)) - } - return nil - default: - return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message())) - } - } - return err -} - -func optionalString(v string) *string { - if v == "" { - return nil - } - return &v -} - -func optionalFloat64(v float64) *float64 { - if v == 0 { - return nil - } - return &v -} - -func optionalBool(v bool) *bool { - if !v { - return nil - } - return &v + return readersclient.New(conn, timeout) } diff --git a/readers/postgres/messages_test.go b/readers/postgres/messages_test.go index dd73310b4..7869d8396 100644 --- a/readers/postgres/messages_test.go +++ b/readers/postgres/messages_test.go @@ -10,7 +10,7 @@ import ( "time" pwriter "github.com/absmach/magistrala/consumers/writers/postgres" - "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/absmach/magistrala/pkg/transformers/json" "github.com/absmach/magistrala/pkg/transformers/senml" "github.com/absmach/magistrala/readers" diff --git a/readers/timescale/messages_test.go b/readers/timescale/messages_test.go index 0ca828d91..93103e62a 100644 --- a/readers/timescale/messages_test.go +++ b/readers/timescale/messages_test.go @@ -10,7 +10,7 @@ import ( "time" twriter "github.com/absmach/magistrala/consumers/writers/timescale" - "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/testsutil" "github.com/absmach/magistrala/pkg/transformers/json" "github.com/absmach/magistrala/pkg/transformers/senml" "github.com/absmach/magistrala/readers" diff --git a/reports/README.md b/reports/README.md deleted file mode 100644 index 57a055fd7..000000000 --- a/reports/README.md +++ /dev/null @@ -1,344 +0,0 @@ -# Reports - -The Reports service generates time-series reports from stored messages. It fetches data from the readers gRPC service, formats results as JSON, CSV, or PDF, optionally emails the report, and supports scheduled report delivery. - -## Configuration - -The service is configured using the following environment variables (values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) where available, otherwise from service defaults): - -### Core service - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_REPORTS_LOG_LEVEL` | Log level for the service | `debug` | -| `MG_REPORTS_HTTP_HOST` | HTTP host to bind | `reports` | -| `MG_REPORTS_HTTP_PORT` | HTTP port to bind | `9017` | -| `MG_REPORTS_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" | -| `MG_REPORTS_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" | -| `MG_REPORTS_INSTANCE_ID` | Instance ID for tracing/health | "" | -| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` | -| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` | -| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` | -| `MG_MESSAGE_BROKER_URL` | Message broker URL (parsed, currently unused by reports) | `nats://nats:4222` | -| `MG_ES_URL` | Event store URL (parsed, currently unused by reports) | `nats://nats:4222` | - -### Database - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_REPORTS_DB_HOST` | PostgreSQL host | `reports-db` | -| `MG_REPORTS_DB_PORT` | PostgreSQL port | `5432` | -| `MG_REPORTS_DB_USER` | PostgreSQL user | `magistrala` | -| `MG_REPORTS_DB_PASS` | PostgreSQL password | `magistrala` | -| `MG_REPORTS_DB_NAME` | PostgreSQL database name | `reports` | -| `MG_REPORTS_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` | -| `MG_REPORTS_DB_SSL_CERT` | PostgreSQL SSL client cert | "" | -| `MG_REPORTS_DB_SSL_KEY` | PostgreSQL SSL client key | "" | -| `MG_REPORTS_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" | - -### Atom - -| Variable | Description | Default | -| --- | --- | --- | -| `ATOM_URL` | Atom HTTP endpoint | `http://atom:8080` | -| `ATOM_JWKS_URL` | Atom JWKS endpoint for JWT verification | `http://atom:8080/.well-known/jwks.json` | -| `ATOM_ADMIN_USERNAME` | Atom admin login for service projections | `atom-admin` | -| `ATOM_ADMIN_SECRET` | Atom admin secret for service projections | `change-me` | -| `ATOM_TIMEOUT` | Atom request timeout | `5s` | -| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` | -| `MG_SPICEDB_PRE_SHARED_KEY` | SpiceDB pre-shared key | `12345678` | -| `MG_SPICEDB_HOST` | SpiceDB host | `magistrala-spicedb` | -| `MG_SPICEDB_PORT` | SpiceDB gRPC port | `50051` | - -### Readers gRPC - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_TIMESCALE_READER_GRPC_URL` | Readers gRPC endpoint | `timescale-reader:7011` | -| `MG_TIMESCALE_READER_GRPC_TIMEOUT` | Readers gRPC timeout | `300s` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_CERT` | Readers gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/reader-grpc-client.crt}` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_CA_CERTS` | Readers gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` | -| `MG_TIMESCALE_READER_GRPC_CLIENT_KEY` | Readers gRPC client key path | `${GRPC_MTLS:+./ssl/certs/readers-grpc-client.key}` | - -### Email - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_EMAIL_HOST` | SMTP host | `smtp.mailtrap.io` | -| `MG_EMAIL_PORT` | SMTP port | `2525` | -| `MG_EMAIL_USERNAME` | SMTP username | `18bf7f70705139` | -| `MG_EMAIL_PASSWORD` | SMTP password | `2b0d302e775b1e` | -| `MG_EMAIL_FROM_ADDRESS` | Sender email address | `from@example.com` | -| `MG_EMAIL_FROM_NAME` | Sender display name | `Example` | -| `MG_EMAIL_TEMPLATE` | Email template path | `email.tmpl` | -| `MG_REPORTS_EMAIL_TEMPLATE` | Template file mounted by Docker Compose | `reports.tmpl` | - -### Templates and PDF conversion - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_REPORTS_DEFAULT_TEMPLATE` | Use on-disk HTML template when non-empty | "" | -| `MG_PDF_CONVERTER_URL` | HTML-to-PDF conversion endpoint | `http://pdf-generator:3000/forms/chromium/convert/html` | - -### Callout - -| Variable | Description | Default | -| --- | --- | --- | -| `MG_REPORTS_CALLOUT_URLS` | Callout target URLs | "" | -| `MG_REPORTS_CALLOUT_METHOD` | Callout HTTP method | `POST` | -| `MG_REPORTS_CALLOUT_TLS_VERIFICATION` | TLS verification for callout | `false` | -| `MG_REPORTS_CALLOUT_TIMEOUT` | Callout timeout | `10s` | -| `MG_REPORTS_CALLOUT_CA_CERT` | Callout CA cert path | "" | -| `MG_REPORTS_CALLOUT_CERT` | Callout client cert path | "" | -| `MG_REPORTS_CALLOUT_KEY` | Callout client key path | "" | -| `MG_REPORTS_CALLOUT_OPERATIONS` | Callout operations filter | "" | - -## Features - -- **Report generation**: Build report data from time-series messages. -- **Multiple formats**: JSON responses, CSV exports, and PDF rendering. -- **Scheduling**: Periodic report delivery via email. -- **Template support**: Custom HTML templates for PDF reports. -- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support. - -## Architecture - -### Runtime flow - -1. The Reports API receives a report request or a scheduled run triggers report generation. -2. The service expands requested metrics and fetches messages via the readers gRPC API in batches of 1000. -3. Results are grouped by publisher when `client_ids` are not specified. -4. Output is returned as JSON, rendered to CSV, or converted to PDF via `MG_PDF_CONVERTER_URL`. -5. For scheduled/email actions, the report is sent as an email attachment. - -### Scheduling - -The scheduler runs on a 30-second ticker and selects enabled report configs with `due` time earlier than now. It updates `due` using `Schedule.NextDue()` and generates a report with the `email` action. - -Recurring types are: `none`, `hourly`, `daily`, `weekly`, `monthly`. The `recurring_period` controls the interval (1 = every interval, 2 = every second interval, etc.). - -### Templates - -PDF templates are Go `html/template` documents. A template must include: - -- `{{$.Title}}` -- `{{range .Messages}}` or `{{range .Reports}}` -- `{{formatTime .Time}}` -- `{{formatValue .}}` -- `{{end}}` - -Helper functions include `formatTime`, `formatValue`, `add`, `sub`, `div`, `mod`, `iterate`, `eq`, `ge`, `lt`, `getStartRow`, and `getEndRow`. - -## Data model - -### report_config table - -Defined in `reports/postgres/init.go`: - -| Column | Type | Description | -| --- | --- | --- | -| `id` | `VARCHAR(36)` | Report config UUID (primary key) | -| `name` | `VARCHAR(1024)` | Report name | -| `description` | `TEXT` | Report description | -| `domain_id` | `VARCHAR(36)` | Domain ID | -| `status` | `SMALLINT` | 0 = enabled, 1 = disabled, 2 = deleted | -| `created_at` | `TIMESTAMP` | Creation timestamp | -| `created_by` | `VARCHAR(254)` | Creator user ID | -| `updated_at` | `TIMESTAMP` | Last update timestamp | -| `updated_by` | `VARCHAR(254)` | Last updater user ID | -| `due` | `TIMESTAMPTZ` | Next scheduled execution time | -| `recurring` | `SMALLINT` | Recurring type | -| `recurring_period` | `SMALLINT` | Recurring period | -| `start_datetime` | `TIMESTAMP` | Schedule start time | -| `config` | `JSONB` | Metric config (from/to/title/format/aggregation) | -| `email` | `JSONB` | Email settings | -| `metrics` | `JSONB` | Requested metrics list | -| `report_template` | `TEXT` | Custom HTML template | - -## Deployment - -### Build and run locally - -```bash -make reports - -MG_REPORTS_LOG_LEVEL=debug \ -MG_REPORTS_HTTP_PORT=9017 \ -MG_REPORTS_DB_HOST=localhost \ -MG_REPORTS_DB_PORT=5432 \ -MG_REPORTS_DB_USER=magistrala \ -MG_REPORTS_DB_PASS=magistrala \ -MG_REPORTS_DB_NAME=reports \ -MG_PDF_CONVERTER_URL=http://localhost:4000/forms/chromium/convert/html \ -MG_AUTH_GRPC_URL=localhost:7001 \ -MG_AUTH_GRPC_TIMEOUT=300s \ -MG_DOMAINS_GRPC_URL=localhost:7003 \ -MG_DOMAINS_GRPC_TIMEOUT=300s \ -MG_TIMESCALE_READER_GRPC_URL=localhost:7011 \ -MG_TIMESCALE_READER_GRPC_TIMEOUT=300s \ -./build/reports -``` - -### Docker Compose - -The service is available as a Docker container. Refer to [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) for the `reports`, `reports-db`, and `pdf-generator` services and their environment variables. For a full local stack, ensure auth, domains, readers, and the PDF generator are running. - -```bash -docker compose -f docker/docker-compose.yaml up reports reports-db pdf-generator -``` - -### Health check - -```bash -curl -X GET http://localhost:9017/health \ - -H "accept: application/health+json" -``` - -## Testing - -```bash -go test ./reports/... -``` - -## Usage - -The Reports service supports the following operations: - -| Operation | Method & Path | Description | -| --- | --- | --- | -| `generateReport` | `POST /{domainID}/reports` | Generate a report (`action` query param) | -| `addReportConfig` | `POST /{domainID}/reports/configs` | Create a report configuration | -| `listReportsConfig` | `GET /{domainID}/reports/configs` | List report configurations | -| `viewReportConfig` | `GET /{domainID}/reports/configs/{reportID}` | View a report configuration | -| `updateReportConfig` | `PATCH /{domainID}/reports/configs/{reportID}` | Update a report configuration | -| `updateReportSchedule` | `PATCH /{domainID}/reports/configs/{reportID}/schedule` | Update schedule | -| `enableReportConfig` | `POST /{domainID}/reports/configs/{reportID}/enable` | Enable a report configuration | -| `disableReportConfig` | `POST /{domainID}/reports/configs/{reportID}/disable` | Disable a report configuration | -| `deleteReportConfig` | `DELETE /{domainID}/reports/configs/{reportID}` | Delete a report configuration | -| `updateReportTemplate` | `PUT /{domainID}/reports/configs/{reportID}/template` | Update custom template | -| `viewReportTemplate` | `GET /{domainID}/reports/configs/{reportID}/template` | View custom template | -| `deleteReportTemplate` | `DELETE /{domainID}/reports/configs/{reportID}/template` | Delete custom template | -| `health` | `GET /health` | Service health check | - -List filters: `offset`, `limit`, `status`, `name`, `order` (`name`, `created_at`, `updated_at`), and `dir` (`asc`, `desc`). - -Time ranges use relative expressions parsed by `pkg/reltime`, such as `now()` or `now()-24h` (units: `s`, `m`, `h`, `d`, `w`). Aggregation intervals use Go duration strings like `15m` or `1h`. File output formats are `pdf` and `csv`. -When metric `subtopic` is used, provide it in slash-delimited form (for example, `sensor/temp`). - -### Example: Generate a report - -```bash -curl -X POST "http://localhost:9017//reports?action=view" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "temperature-view", - "metrics": [ - { - "channel_id": "", - "client_ids": [""], - "name": "temperature", - "subtopic": "sensor" - } - ], - "config": { - "from": "now()-24h", - "to": "now()", - "title": "Temperature (last 24h)", - "timezone": "UTC", - "aggregation": { - "agg_type": "avg", - "interval": "1h" - } - } - }' -``` - -### Example: Generate and email a report - -```bash -curl -X POST "http://localhost:9017//reports?action=email" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "temperature-email", - "metrics": [ - { - "channel_id": "", - "name": "temperature" - } - ], - "config": { - "from": "now()-1d", - "to": "now()", - "title": "Daily Temperature", - "file_format": "csv" - }, - "email": { - "to": ["ops@example.com"], - "subject": "Daily temperature report", - "content": "Report attached." - } - }' - -``` - -### Example: Create a scheduled report config - -```bash -curl -X POST "http://localhost:9017//reports/configs" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "daily-temperature", - "description": "Daily temperature summary", - "metrics": [ - { - "channel_id": "", - "name": "temperature" - } - ], - "config": { - "from": "now()-1d", - "to": "now()", - "title": "Daily Temperature", - "file_format": "pdf", - "aggregation": { - "agg_type": "avg", - "interval": "1h" - } - }, - "email": { - "to": ["ops@example.com"], - "subject": "Daily temperature report", - "content": "Report attached." - }, - "schedule": { - "start_datetime": "2025-01-01T00:00:00Z", - "recurring": "daily", - "recurring_period": 1 - } - }' -``` - -### Example: Update a report template - -```bash -curl -X PUT "http://localhost:9017//reports/configs//template" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "report_template": "

{{$.Title}}

{{range .Reports}}{{range .Messages}}{{formatTime .Time}} {{formatValue .}}{{end}}{{end}}" - }' -``` - -### Example: Enable a report config - -```bash -curl -X POST "http://localhost:9017//reports/configs//enable" \ - -H "Authorization: Bearer " -``` - -For an in-depth explanation of our Reports Service, see the see the [official documentation][doc]. - -[doc]: https://magistrala.absmach.eu/docs/user-guide/reports/ diff --git a/reports/api/doc.go b/reports/api/doc.go deleted file mode 100644 index 2424852cc..000000000 --- a/reports/api/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package api contains API-related concerns: endpoint definitions, middlewares -// and all resource representations. -package api diff --git a/reports/api/endpoints.go b/reports/api/endpoints.go deleted file mode 100644 index 234e0b5ec..000000000 --- a/reports/api/endpoints.go +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/reports" - "github.com/go-kit/kit/endpoint" -) - -func generateReportEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(generateReportReq) - if err := req.validate(); err != nil { - return generateReportResp{}, err - } - - res, err := svc.GenerateReport(ctx, session, req.ReportConfig, req.action) - if err != nil { - return generateReportResp{}, err - } - - switch req.action { - case reports.DownloadReport: - return downloadReportResp{ - File: res.File, - }, nil - case reports.EmailReport: - return emailReportResp{}, nil - default: - return generateReportResp{ - Total: res.Total, - From: res.From, - To: res.To, - Aggregation: res.Aggregation, - Reports: res.Reports, - }, nil - } - } -} - -func listReportsConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(listReportsConfigReq) - if err := req.validate(); err != nil { - return listReportsConfigRes{}, err - } - - page, err := svc.ListReportsConfig(ctx, session, req.PageMeta) - if err != nil { - return listReportsConfigRes{}, err - } - - return listReportsConfigRes{ - pageRes: pageRes{ - Limit: page.Limit, - Offset: page.Offset, - Total: page.Total, - }, - ReportConfigs: page.ReportConfigs, - }, nil - } -} - -func deleteReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(deleteReportConfigReq) - if err := req.validate(); err != nil { - return deleteReportConfigRes{}, err - } - - err := svc.RemoveReportConfig(ctx, session, req.ID) - if err != nil { - return deleteReportConfigRes{false}, err - } - - return deleteReportConfigRes{true}, nil - } -} - -func updateReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportConfigReq) - if err := req.validate(); err != nil { - return updateReportConfigRes{}, err - } - - cfg, err := svc.UpdateReportConfig(ctx, session, req.ReportConfig) - if err != nil { - return updateReportConfigRes{}, err - } - - return updateReportConfigRes{ReportConfig: cfg}, nil - } -} - -func updateReportScheduleEndpoint(s reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportScheduleReq) - if err := req.validate(); err != nil { - return updateReportConfigRes{}, err - } - - rpt := reports.ReportConfig{ - ID: req.id, - Schedule: req.Schedule, - } - - updatedReport, err := s.UpdateReportSchedule(ctx, session, rpt) - if err != nil { - return updateReportConfigRes{}, err - } - return updateReportConfigRes{ReportConfig: updatedReport}, nil - } -} - -func viewReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(viewReportConfigReq) - if err := req.validate(); err != nil { - return viewReportConfigRes{}, err - } - - cfg, err := svc.ViewReportConfig(ctx, session, req.ID, req.withRoles) - if err != nil { - return viewReportConfigRes{}, err - } - - return viewReportConfigRes{ReportConfig: cfg}, nil - } -} - -func addReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(addReportConfigReq) - if err := req.validate(); err != nil { - return addReportConfigRes{}, err - } - - cfg, err := svc.AddReportConfig(ctx, session, req.ReportConfig) - if err != nil { - return addReportConfigRes{}, err - } - - return addReportConfigRes{ - ReportConfig: cfg, - created: true, - }, nil - } -} - -func enableReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportStatusReq) - if err := req.validate(); err != nil { - return updateReportConfigRes{}, err - } - - cfg, err := svc.EnableReportConfig(ctx, session, req.id) - if err != nil { - return updateReportConfigRes{}, err - } - - return updateReportConfigRes{ReportConfig: cfg}, nil - } -} - -func disableReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportStatusReq) - if err := req.validate(); err != nil { - return updateReportConfigRes{}, err - } - - cfg, err := svc.DisableReportConfig(ctx, session, req.id) - if err != nil { - return updateReportConfigRes{}, err - } - - return updateReportConfigRes{ReportConfig: cfg}, nil - } -} - -func updateReportTemplateEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportTemplateReq) - if err := req.validate(); err != nil { - return updateReportTemplateRes{false}, err - } - - err := svc.UpdateReportTemplate(ctx, session, req.ReportConfig) - if err != nil { - return updateReportTemplateRes{false}, err - } - - return updateReportTemplateRes{true}, nil - } -} - -func viewReportTemplateEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(getReportTemplateReq) - if err := req.validate(); err != nil { - return viewReportTemplateRes{}, err - } - - template, err := svc.ViewReportTemplate(ctx, session, req.ID) - if err != nil { - return viewReportTemplateRes{}, err - } - - return viewReportTemplateRes{Template: template}, nil - } -} - -func deleteReportTemplateEndpoint(svc reports.Service) endpoint.Endpoint { - return func(ctx context.Context, request any) (any, error) { - session, ok := ctx.Value(authn.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(deleteReportTemplateReq) - if err := req.validate(); err != nil { - return deleteReportTemplateRes{false}, err - } - - err := svc.DeleteReportTemplate(ctx, session, req.ID) - if err != nil { - return deleteReportTemplateRes{false}, err - } - - return deleteReportTemplateRes{true}, nil - } -} diff --git a/reports/api/endpoints_test.go b/reports/api/endpoints_test.go deleted file mode 100644 index 2d912fdd9..000000000 --- a/reports/api/endpoints_test.go +++ /dev/null @@ -1,1404 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api_test - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/testsutil" - mglog "github.com/absmach/magistrala/logger" - smqauthn "github.com/absmach/magistrala/pkg/authn" - authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkgSch "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/api" - "github.com/absmach/magistrala/reports/mocks" - "github.com/go-chi/chi/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const contentType = "application/json" - -var ( - namegen = namegenerator.NewGenerator() - domainID = testsutil.GenerateUUID(&testing.T{}) - userID = testsutil.GenerateUUID(&testing.T{}) - validID = testsutil.GenerateUUID(&testing.T{}) - validToken = "valid" - invalidToken = "invalid" - now = time.Now().UTC().Truncate(time.Minute) - future = now.Add(1 * time.Hour) - schedule = pkgSch.Schedule{ - StartDateTime: future, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: future, - } - reportConfig = reports.ReportConfig{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Schedule: schedule, - Status: reports.EnabledStatus, - Metrics: []reports.ReqMetric{ - { - ChannelID: "channel1", - ClientIDs: []string{"client1"}, - Name: "metric_name", - }, - }, - Config: &reports.MetricConfig{ - From: "now()-1h", - To: "now()", - Title: title, - Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"}, - }, - Email: &reports.EmailSetting{ - To: []string{"test@example.com"}, - Subject: "Test Report", - }, - } - title = "test_title" -) - -type testRequest struct { - client *http.Client - method string - url string - contentType string - token string - body io.Reader -} - -func (tr testRequest) make() (*http.Response, error) { - req, err := http.NewRequest(tr.method, tr.url, tr.body) - if err != nil { - return nil, err - } - - if tr.token != "" { - req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token) - } - - if tr.contentType != "" { - req.Header.Set("Content-Type", tr.contentType) - } - - req.Header.Set("Referer", "http://localhost") - - return tr.client.Do(req) -} - -func newReportsServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { - svc := new(mocks.Service) - authn := new(authnmocks.Authentication) - - logger := mglog.NewMock() - mux := chi.NewRouter() - am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true)) - - api.MakeHandler(svc, am, mux, logger, "") - - return httptest.NewServer(mux), svc, authn -} - -func toJSON(data any) string { - jsonData, err := json.Marshal(data) - if err != nil { - return "" - } - return string(jsonData) -} - -func TestAddReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - scheduleInPast := pkgSch.Schedule{ - StartDateTime: now, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: now, - } - - reportInPast := reportConfig - reportInPast.Schedule = scheduleInPast - - cases := []struct { - desc string - cfg reports.ReportConfig - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes reports.ReportConfig - svcErr error - err error - }{ - { - desc: "add report config successfully", - cfg: reportConfig, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusCreated, - svcRes: reportConfig, - }, - { - desc: "add report config with invalid token", - cfg: reportConfig, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "add report config with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - cfg: reportConfig, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "add report config with empty domainID", - token: validToken, - cfg: reportConfig, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "add report config with invalid content type", - token: validToken, - domainID: domainID, - cfg: reportConfig, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "add report config with startdatetime in past", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - cfg: reportInPast, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "add report config with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - cfg: reportConfig, - contentType: contentType, - svcErr: svcerr.ErrCreateEntity, - status: http.StatusUnprocessableEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.cfg) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/reports/configs", ts.URL, tc.domainID), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("AddReportConfig", mock.Anything, tc.authnRes, mock.Anything).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestViewReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - id string - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes reports.ReportConfig - svcErr error - err error - }{ - { - desc: "view report config successfully", - id: validID, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusOK, - svcRes: reportConfig, - }, - { - desc: "view report config with invalid token", - id: validID, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "view report config with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - id: validID, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "view report config with empty domainID", - token: validToken, - id: validID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "view report config with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - id: validID, - contentType: contentType, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("ViewReportConfig", mock.Anything, tc.authnRes, tc.id, false).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestListReportsConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - query string - domainID string - token string - session smqauthn.Session - listReportsResponse reports.ReportConfigPage - status int - authnErr error - err error - }{ - { - desc: "list reports config successfully", - domainID: domainID, - token: validToken, - status: http.StatusOK, - listReportsResponse: reports.ReportConfigPage{ - ReportConfigs: []reports.ReportConfig{reportConfig}, - PageMeta: reports.PageMeta{Total: 1}, - }, - err: nil, - }, - { - desc: "list reports config with empty token", - domainID: domainID, - token: "", - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "list reports config with invalid token", - domainID: domainID, - token: invalidToken, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: ts.URL + "/" + tc.domainID + "/reports/configs?" + tc.query, - contentType: contentType, - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("ListReportsConfig", mock.Anything, tc.session, mock.Anything).Return(tc.listReportsResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var bodyRes respBody - err = json.NewDecoder(res.Body).Decode(&bodyRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if bodyRes.Err != "" || bodyRes.Message != "" { - err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestUpdateReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - updateReq reports.ReportConfig - contentType string - session smqauthn.Session - svcResp reports.ReportConfig - svcErr error - status int - authnErr error - err error - }{ - { - desc: "update report config successfully", - token: validToken, - domainID: domainID, - id: validID, - updateReq: reportConfig, - contentType: contentType, - svcResp: reportConfig, - status: http.StatusOK, - err: nil, - }, - { - desc: "update report config with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - updateReq: reportConfig, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "update report config with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - updateReq: reportConfig, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "update report config with empty domainID", - token: validToken, - id: validID, - updateReq: reportConfig, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "update report config with invalid content type", - token: validToken, - id: validID, - domainID: domainID, - updateReq: reportConfig, - contentType: "application/xml", - svcResp: reportConfig, - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "update report config with service error", - token: validToken, - id: validID, - domainID: domainID, - updateReq: reportConfig, - contentType: contentType, - svcResp: reports.ReportConfig{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.updateReq) - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("UpdateReportConfig", mock.Anything, tc.session, mock.Anything).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestDeleteReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcErr error - status int - authnErr error - err error - }{ - { - desc: "delete report config successfully", - token: validToken, - domainID: domainID, - id: validID, - svcErr: nil, - status: http.StatusNoContent, - err: nil, - }, - { - desc: "delete report config with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "delete report config with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "delete report config with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "delete report config with service error", - token: validToken, - id: validID, - domainID: domainID, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodDelete, - url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("RemoveReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestEnableReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp reports.ReportConfig - svcErr error - status int - authnErr error - err error - }{ - { - desc: "enable report config successfully", - token: validToken, - domainID: domainID, - id: validID, - svcResp: reportConfig, - svcErr: nil, - status: http.StatusOK, - err: nil, - }, - { - desc: "enable report config with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "enable report config with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "enable report config with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "enable report config with service error", - token: validToken, - id: validID, - domainID: domainID, - svcResp: reports.ReportConfig{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "enable report config with empty id", - token: validToken, - id: "", - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/reports/configs/%s/enable", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("EnableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestDisableReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp reports.ReportConfig - svcErr error - status int - authnErr error - err error - }{ - { - desc: "disable report config successfully", - token: validToken, - domainID: domainID, - id: validID, - svcResp: reportConfig, - svcErr: nil, - status: http.StatusOK, - err: nil, - }, - { - desc: "disable report config with invalid token", - token: invalidToken, - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "disable report config with empty token", - token: "", - session: smqauthn.Session{}, - domainID: domainID, - id: validID, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "disable report config with empty domainID", - token: validToken, - id: validID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "disable report config with service error", - token: validToken, - id: validID, - domainID: domainID, - svcResp: reports.ReportConfig{}, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - { - desc: "disable report config with empty id", - token: validToken, - id: "", - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/reports/configs/%s/disable", ts.URL, tc.domainID, tc.id), - token: tc.token, - } - if tc.token == validToken { - tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID} - } - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr) - svcCall := svc.On("DisableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -type respBody struct { - Err string `json:"error"` - Message string `json:"message"` - Total uint64 `json:"total"` - ID string `json:"id"` - Status reports.Status `json:"status"` -} - -const ( - validTemplate = ` - - - {{$.Title}} - - - -
-

{{$.Title}}

-

Generated on: {{$.GeneratedDate}}

-
-
-

Messages

- {{range .Messages}} -
-

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

-
- {{end}} -
- -` - - templateWithoutTitle = ` - - - Report - - - -

Report

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end}} - -` - - templateWithSyntaxError = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end - -` -) - -func TestUpdateReportTemplateEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - id string - template reports.ReportTemplate - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcErr error - err error - }{ - { - desc: "update report template successfully", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusNoContent, - }, - { - desc: "update report template with invalid token", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "update report template with empty token", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "update report template with empty domainID", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: validToken, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "update report template with invalid content type", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: validToken, - domainID: domainID, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "update report template with empty ID", - id: "", - template: reports.ReportTemplate(validTemplate), - token: validToken, - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - { - desc: "update report template with empty template", - id: validID, - token: validToken, - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "update report template without title field", - id: validID, - template: reports.ReportTemplate(templateWithoutTitle), - token: validToken, - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "update report template with syntax error", - id: validID, - template: reports.ReportTemplate(templateWithSyntaxError), - token: validToken, - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "update report template with service error", - id: validID, - template: reports.ReportTemplate(validTemplate), - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - contentType: contentType, - svcErr: svcerr.ErrUpdateEntity, - status: http.StatusUnprocessableEntity, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(map[string]any{ - "report_template": tc.template, - }) - req := testRequest{ - client: ts.Client(), - method: http.MethodPut, - url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("UpdateReportTemplate", mock.Anything, tc.authnRes, mock.Anything).Return(tc.svcErr) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - if res.StatusCode != http.StatusNoContent { - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - } - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestViewReportTemplateEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - id string - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes reports.ReportTemplate - svcErr error - err error - }{ - { - desc: "view report template successfully", - id: validID, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusOK, - svcRes: reports.ReportTemplate(validTemplate), - }, - { - desc: "view report template with invalid token", - id: validID, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "view report template with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - id: validID, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "view report template with empty domainID", - token: validToken, - id: validID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "view report template with empty ID", - token: validToken, - id: "", - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - { - desc: "view report template with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - id: validID, - contentType: contentType, - svcErr: svcerr.ErrViewEntity, - status: http.StatusUnprocessableEntity, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("ViewReportTemplate", mock.Anything, tc.authnRes, tc.id).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestDeleteReportTemplateEndpoint(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - id string - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcErr error - err error - }{ - { - desc: "delete report template successfully", - id: validID, - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusNoContent, - }, - { - desc: "delete report template with invalid token", - id: validID, - token: invalidToken, - authnRes: smqauthn.Session{}, - domainID: domainID, - contentType: contentType, - authnErr: svcerr.ErrAuthentication, - status: http.StatusUnauthorized, - err: svcerr.ErrAuthentication, - }, - { - desc: "delete report template with empty token", - token: "", - authnRes: smqauthn.Session{}, - domainID: domainID, - id: validID, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "delete report template with empty domainID", - token: validToken, - id: validID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingDomainID, - }, - { - desc: "delete report template with empty ID", - token: validToken, - id: "", - domainID: domainID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrMissingID, - }, - { - desc: "delete report template with service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - id: validID, - contentType: contentType, - svcErr: svcerr.ErrRemoveEntity, - status: http.StatusUnprocessableEntity, - err: svcerr.ErrRemoveEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodDelete, - url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id), - contentType: tc.contentType, - token: tc.token, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("DeleteReportTemplate", mock.Anything, tc.authnRes, tc.id).Return(tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - - if res.StatusCode != http.StatusNoContent { - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - } - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} - -func TestGenerateReportWithTemplateValidation(t *testing.T) { - ts, svc, authn := newReportsServer() - defer ts.Close() - - cases := []struct { - desc string - cfg reports.ReportConfig - action string - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes reports.ReportPage - svcErr error - err error - }{ - { - desc: "generate report with valid template successfully", - cfg: reports.ReportConfig{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Metrics: []reports.ReqMetric{ - { - ChannelID: "channel1", - ClientIDs: []string{"client1"}, - Name: "metric_name", - }, - }, - Config: &reports.MetricConfig{ - From: "now()-1h", - To: "now()", - Title: title, - Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"}, - }, - ReportTemplate: reports.ReportTemplate(validTemplate), - }, - action: "view", - token: validToken, - contentType: contentType, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - status: http.StatusOK, - svcRes: reports.ReportPage{}, - }, - { - desc: "generate report with invalid template", - cfg: reports.ReportConfig{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Metrics: []reports.ReqMetric{ - { - ChannelID: "channel1", - ClientIDs: []string{"client1"}, - Name: "metric_name", - }, - }, - Config: &reports.MetricConfig{ - From: "now()-1h", - To: "now()", - Title: title, - Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"}, - }, - ReportTemplate: reports.ReportTemplate(templateWithoutTitle), - }, - action: "view", - token: validToken, - contentType: contentType, - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "generate report with template syntax error", - cfg: reports.ReportConfig{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Metrics: []reports.ReqMetric{ - { - ChannelID: "channel1", - ClientIDs: []string{"client1"}, - Name: "metric_name", - }, - }, - Config: &reports.MetricConfig{ - From: "now()-1h", - To: "now()", - Title: title, - Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"}, - }, - ReportTemplate: reports.ReportTemplate(templateWithSyntaxError), - }, - action: "view", - token: validToken, - contentType: contentType, - domainID: domainID, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.cfg) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/reports?action=%s", ts.URL, tc.domainID, tc.action), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("GenerateReport", mock.Anything, tc.authnRes, mock.Anything, mock.Anything).Return(tc.svcRes, tc.svcErr) - res, err := req.make() - - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authCall.Unset() - }) - } -} diff --git a/reports/api/request.go b/reports/api/request.go deleted file mode 100644 index 81d54ee54..000000000 --- a/reports/api/request.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "fmt" - - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/reports" -) - -const ( - maxLimitSize = 1000 - MaxNameSize = 1024 - MaxTitleSize = 37 - - errInvalidMetric = "invalid metric[%d]: %w" -) - -var ( - errInvalidReportAction = errors.New("invalid report action") - errMetricsNotProvided = errors.New("metrics not provided") - errMissingReportConfig = errors.New("missing report config") - errMissingReportEmailConfig = errors.New("missing report email config") - errInvalidRecurringPeriod = errors.New("invalid recurring period") - errMissingReportTemplate = errors.New("missing report template") - errTitleSize = errors.New("invalid title size") -) - -type addReportConfigReq struct { - reports.ReportConfig `json:",inline"` -} - -func (req addReportConfigReq) validate() error { - if req.Name == "" { - return apiutil.ErrMissingName - } - if err := req.Schedule.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - if req.ReportTemplate.String() != "" { - if err := req.ReportTemplate.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - } - return validateReportConfig(req.ReportConfig, false, false) -} - -type viewReportConfigReq struct { - ID string `json:"id"` - withRoles bool -} - -func (req viewReportConfigReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return nil -} - -type listReportsConfigReq struct { - reports.PageMeta `json:",inline"` -} - -func (req listReportsConfigReq) validate() error { - if req.Limit > maxLimitSize { - return svcerr.ErrMalformedEntity - } - - switch req.Order { - case "", api.NameKey, api.CreatedAtOrder, api.UpdatedAtOrder: - default: - return apiutil.ErrInvalidOrder - } - - if req.Dir != api.AscDir && req.Dir != api.DescDir { - return apiutil.ErrInvalidDirection - } - - return nil -} - -type updateReportConfigReq struct { - reports.ReportConfig `json:",inline"` -} - -func (req updateReportConfigReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return validateReportConfig(req.ReportConfig, false, false) -} - -type updateReportScheduleReq struct { - id string - Schedule schedule.Schedule `json:"schedule,omitempty"` -} - -func (req updateReportScheduleReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - if err := req.Schedule.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - - return nil -} - -type deleteReportConfigReq struct { - ID string `json:"id"` -} - -func (req deleteReportConfigReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return nil -} - -type generateReportReq struct { - reports.ReportConfig - action reports.ReportAction -} - -func (req generateReportReq) validate() error { - if len(req.Config.Title) > MaxTitleSize { - return errors.Wrap(apiutil.ErrValidation, errTitleSize) - } - - if req.ReportTemplate.String() != "" { - if err := req.ReportTemplate.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - } - - switch req.action { - case reports.ViewReport, reports.DownloadReport: - return validateReportConfig(req.ReportConfig, true, true) - case reports.EmailReport: - return validateReportConfig(req.ReportConfig, false, true) - default: - return errors.Wrap(apiutil.ErrValidation, errInvalidReportAction) - } -} - -type updateReportStatusReq struct { - id string -} - -func (req updateReportStatusReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - return nil -} - -func validateReportConfig(req reports.ReportConfig, skipEmailValidation bool, skipSchedularValidation bool) error { - if len(req.Metrics) == 0 { - return errors.Wrap(apiutil.ErrValidation, errMetricsNotProvided) - } - for i, metric := range req.Metrics { - if err := metric.Validate(); err != nil { - return errors.Wrap(apiutil.ErrValidation, fmt.Errorf(errInvalidMetric, i+1, err)) - } - } - - if req.Config == nil { - return errors.Wrap(errMissingReportConfig, apiutil.ErrValidation) - } - if err := req.Config.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - - if skipEmailValidation { - return nil - } - if req.Email == nil { - return errors.Wrap(errMissingReportEmailConfig, apiutil.ErrValidation) - } - if err := req.Email.Validate(); err != nil { - return errors.Wrap(apiutil.ErrValidation, err) - } - - if skipSchedularValidation { - return nil - } - - return validateScheduler(req.Schedule) -} - -func validateScheduler(sch schedule.Schedule) error { - if sch.Recurring != schedule.None && sch.RecurringPeriod < 1 { - return errInvalidRecurringPeriod - } - return nil -} - -type updateReportTemplateReq struct { - reports.ReportConfig `json:",inline"` -} - -func (req updateReportTemplateReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - if req.ReportTemplate == "" { - return errors.Wrap(errMissingReportTemplate, apiutil.ErrValidation) - } - if err := req.ReportTemplate.Validate(); err != nil { - return errors.Wrap(err, apiutil.ErrValidation) - } - return nil -} - -type getReportTemplateReq struct { - ID string `json:"id"` -} - -func (req getReportTemplateReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return nil -} - -type deleteReportTemplateReq struct { - ID string `json:"id"` -} - -func (req deleteReportTemplateReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return nil -} diff --git a/reports/api/response.go b/reports/api/response.go deleted file mode 100644 index 9699e5cc4..000000000 --- a/reports/api/response.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "net/http" - "time" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/reports" -) - -var ( - _ magistrala.Response = (*addReportConfigRes)(nil) - _ magistrala.Response = (*viewReportConfigRes)(nil) - _ magistrala.Response = (*updateReportConfigRes)(nil) - _ magistrala.Response = (*deleteReportConfigRes)(nil) - _ magistrala.Response = (*listReportsConfigRes)(nil) -) - -type pageRes struct { - Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset"` - Total uint64 `json:"total"` -} - -type generateReportResp struct { - Total uint64 `json:"total"` - From time.Time `json:"from,omitempty"` - To time.Time `json:"to,omitempty"` - Aggregation reports.AggConfig `json:"aggregation,omitempty"` - Reports []reports.Report `json:"reports,omitempty"` -} - -func (res generateReportResp) Code() int { - return http.StatusOK -} - -func (res generateReportResp) Headers() map[string]string { - return map[string]string{} -} - -func (res generateReportResp) Empty() bool { - return false -} - -type addReportConfigRes struct { - reports.ReportConfig `json:",inline"` - created bool -} - -func (res addReportConfigRes) Code() int { - if res.created { - return http.StatusCreated - } - return http.StatusOK -} - -func (res addReportConfigRes) Headers() map[string]string { - if res.created { - return map[string]string{} - } - return map[string]string{} -} - -func (res addReportConfigRes) Empty() bool { - return false -} - -type viewReportConfigRes struct { - reports.ReportConfig `json:",inline"` -} - -func (res viewReportConfigRes) Code() int { - return http.StatusOK -} - -func (res viewReportConfigRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewReportConfigRes) Empty() bool { - return false -} - -type updateReportConfigRes struct { - reports.ReportConfig `json:",inline"` -} - -func (res updateReportConfigRes) Code() int { - return http.StatusOK -} - -func (res updateReportConfigRes) Headers() map[string]string { - return map[string]string{} -} - -func (res updateReportConfigRes) Empty() bool { - return false -} - -type deleteReportConfigRes struct { - deleted bool -} - -func (res deleteReportConfigRes) Code() int { - if res.deleted { - return http.StatusNoContent - } - return http.StatusOK -} - -func (res deleteReportConfigRes) Headers() map[string]string { - return map[string]string{} -} - -func (res deleteReportConfigRes) Empty() bool { - return true -} - -type listReportsConfigRes struct { - pageRes - ReportConfigs []reports.ReportConfig `json:"report_configs"` -} - -func (res listReportsConfigRes) Code() int { - return http.StatusOK -} - -func (res listReportsConfigRes) Headers() map[string]string { - return map[string]string{} -} - -func (res listReportsConfigRes) Empty() bool { - return false -} - -type downloadReportResp struct { - File reports.ReportFile -} - -func (res downloadReportResp) Code() int { - return http.StatusOK -} - -func (res downloadReportResp) Headers() map[string]string { - return map[string]string{} -} - -func (res downloadReportResp) Empty() bool { - return false -} - -type emailReportResp struct{} - -func (res emailReportResp) Code() int { - return http.StatusOK -} - -func (res emailReportResp) Headers() map[string]string { - return map[string]string{} -} - -func (res emailReportResp) Empty() bool { - return true -} - -type viewReportTemplateRes struct { - Template reports.ReportTemplate `json:"html_template"` -} - -func (res viewReportTemplateRes) Code() int { - return http.StatusOK -} - -func (res viewReportTemplateRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewReportTemplateRes) Empty() bool { - return false -} - -type updateReportTemplateRes struct { - updated bool -} - -func (res updateReportTemplateRes) Code() int { - if res.updated { - return http.StatusNoContent - } - return http.StatusOK -} - -func (res updateReportTemplateRes) Headers() map[string]string { - return map[string]string{} -} - -func (res updateReportTemplateRes) Empty() bool { - return true -} - -type deleteReportTemplateRes struct { - deleted bool -} - -func (res deleteReportTemplateRes) Code() int { - if res.deleted { - return http.StatusNoContent - } - return http.StatusOK -} - -func (res deleteReportTemplateRes) Headers() map[string]string { - return map[string]string{} -} - -func (res deleteReportTemplateRes) Empty() bool { - return true -} diff --git a/reports/api/transport.go b/reports/api/transport.go deleted file mode 100644 index dffd2bc0a..000000000 --- a/reports/api/transport.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "strings" - - "github.com/absmach/magistrala" - api "github.com/absmach/magistrala/api/http" - apiutil "github.com/absmach/magistrala/api/http/util" - smqauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/reports" - "github.com/go-chi/chi/v5" - kithttp "github.com/go-kit/kit/transport/http" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -const ( - reportIdKey = "reportID" - actionKey = "action" - defAction = "view" -) - -// MakeHandler creates an HTTP handler for the service endpoints. -func MakeHandler(svc reports.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - mux.Group(func(r chi.Router) { - r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()) - r.Route("/{domainID}", func(r chi.Router) { - r.Route("/reports", func(r chi.Router) { - r.Post("/", otelhttp.NewHandler(kithttp.NewServer( - generateReportEndpoint(svc), - decodeGenerateReportRequest, - encodeFileDownloadResponse, - opts..., - ), "generate_report").ServeHTTP) - - r.Route("/configs", func(r chi.Router) { - r.Post("/", otelhttp.NewHandler(kithttp.NewServer( - addReportConfigEndpoint(svc), - decodeAddReportConfigRequest, - api.EncodeResponse, - opts..., - ), "add_report_config").ServeHTTP) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - listReportsConfigEndpoint(svc), - decodeListReportsConfigRequest, - api.EncodeResponse, - opts..., - ), "list_reports_config").ServeHTTP) - - r.Route("/{reportID}", func(r chi.Router) { - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - viewReportConfigEndpoint(svc), - decodeViewReportConfigRequest, - api.EncodeResponse, - opts..., - ), "view_report_config").ServeHTTP) - - r.Patch("/", otelhttp.NewHandler(kithttp.NewServer( - updateReportConfigEndpoint(svc), - decodeUpdateReportConfigRequest, - api.EncodeResponse, - opts..., - ), "update_report_config").ServeHTTP) - - r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer( - updateReportScheduleEndpoint(svc), - decodeUpdateReportScheduleRequest, - api.EncodeResponse, - opts..., - ), "update_report_scheduler").ServeHTTP) - - r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( - deleteReportConfigEndpoint(svc), - decodeDeleteReportConfigRequest, - api.EncodeResponse, - opts..., - ), "delete_report_config").ServeHTTP) - - r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer( - enableReportConfigEndpoint(svc), - decodeUpdateReportStatusRequest, - api.EncodeResponse, - opts..., - ), "enable_report_config").ServeHTTP) - - r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer( - disableReportConfigEndpoint(svc), - decodeUpdateReportStatusRequest, - api.EncodeResponse, - opts..., - ), "disable_report_config").ServeHTTP) - - r.Put("/template", otelhttp.NewHandler(kithttp.NewServer( - updateReportTemplateEndpoint(svc), - decodeUpdateReportTemplateRequest, - api.EncodeResponse, - opts..., - ), "update_report_template").ServeHTTP) - - r.Get("/template", otelhttp.NewHandler(kithttp.NewServer( - viewReportTemplateEndpoint(svc), - decodeGetReportTemplateRequest, - api.EncodeResponse, - opts..., - ), "get_report_template").ServeHTTP) - - r.Delete("/template", otelhttp.NewHandler(kithttp.NewServer( - deleteReportTemplateEndpoint(svc), - decodeDeleteReportTemplateRequest, - api.EncodeResponse, - opts..., - ), "delete_report_template").ServeHTTP) - }) - }) - }) - }) - }) - - mux.Get("/health", magistrala.Health("reports", instanceID)) - mux.Handle("/metrics", promhttp.Handler()) - - return mux -} - -func decodeGenerateReportRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - - a, err := apiutil.ReadStringQuery(r, actionKey, defAction) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - action, err := reports.ToReportAction(a) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - req := generateReportReq{ - action: action, - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(err, apiutil.ErrValidation) - } - - return req, nil -} - -func decodeAddReportConfigRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - var config reports.ReportConfig - if err := json.NewDecoder(r.Body).Decode(&config); err != nil { - return nil, errors.Wrap(err, apiutil.ErrValidation) - } - return addReportConfigReq{ReportConfig: config}, nil -} - -func decodeViewReportConfigRequest(_ context.Context, r *http.Request) (any, error) { - id := chi.URLParam(r, reportIdKey) - withRoles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false) - if err != nil { - return nil, err - } - return viewReportConfigReq{ID: id, withRoles: withRoles}, nil -} - -func decodeUpdateReportConfigRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - var config reports.ReportConfig - if err := json.NewDecoder(r.Body).Decode(&config); err != nil { - return nil, errors.Wrap(err, apiutil.ErrValidation) - } - config.ID = chi.URLParam(r, reportIdKey) - return updateReportConfigReq{ReportConfig: config}, nil -} - -func decodeUpdateReportScheduleRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - - req := updateReportScheduleReq{ - id: chi.URLParam(r, reportIdKey), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) - } - - return req, nil -} - -func decodeUpdateReportStatusRequest(_ context.Context, r *http.Request) (any, error) { - req := updateReportStatusReq{ - id: chi.URLParam(r, reportIdKey), - } - return req, nil -} - -func decodeDeleteReportConfigRequest(_ context.Context, r *http.Request) (any, error) { - id := chi.URLParam(r, reportIdKey) - return deleteReportConfigReq{ID: id}, nil -} - -func decodeUpdateReportTemplateRequest(_ context.Context, r *http.Request) (any, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, apiutil.ErrUnsupportedContentType - } - - req := updateReportTemplateReq{} - req.ID = chi.URLParam(r, reportIdKey) - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(err, apiutil.ErrValidation) - } - - return req, nil -} - -func decodeGetReportTemplateRequest(_ context.Context, r *http.Request) (any, error) { - return getReportTemplateReq{ID: chi.URLParam(r, reportIdKey)}, nil -} - -func decodeDeleteReportTemplateRequest(_ context.Context, r *http.Request) (any, error) { - return deleteReportTemplateReq{ID: chi.URLParam(r, reportIdKey)}, nil -} - -func decodeListReportsConfigRequest(_ context.Context, r *http.Request) (any, error) { - offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - status, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefStatus) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - st, err := reports.ToStatus(status) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - name, err := apiutil.ReadStringQuery(r, api.NameKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - dir, err := apiutil.ReadStringQuery(r, api.DirKey, "desc") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - return listReportsConfigReq{ - PageMeta: reports.PageMeta{ - Offset: offset, - Limit: limit, - Status: st, - Name: name, - Dir: dir, - Order: order, - }, - }, nil -} - -func encodeFileDownloadResponse(_ context.Context, w http.ResponseWriter, response any) error { - switch resp := response.(type) { - case downloadReportResp: - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", resp.File.Name)) - w.Header().Set("Content-Type", resp.File.Format.ContentType()) - _, err := w.Write(resp.File.Data) - return err - default: - if ar, ok := response.(magistrala.Response); ok { - for k, v := range ar.Headers() { - w.Header().Set(k, v) - } - w.Header().Set("Content-Type", api.ContentType) - w.WriteHeader(ar.Code()) - - if ar.Empty() { - return nil - } - } - return json.NewEncoder(w).Encode(response) - } -} diff --git a/reports/atom.go b/reports/atom.go deleted file mode 100644 index 37cefbcea..000000000 --- a/reports/atom.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "context" - - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" -) - -type atomService struct { - Service - projector atom.Projector -} - -func WithAtom(svc Service, projector atom.Projector) Service { - if projector == nil { - return svc - } - return atomService{Service: svc, projector: projector} -} - -func (svc atomService) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - report, err := svc.Service.AddReportConfig(ctx, session, cfg) - if err != nil { - return report, err - } - if err := svc.projector.UpsertResource(ctx, reportProjection(report)); err != nil { - return report, nil - } - return report, nil -} - -func (svc atomService) UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - report, err := svc.Service.UpdateReportConfig(ctx, session, cfg) - return svc.upsertAfterReportChange(ctx, report, err) -} - -func (svc atomService) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - report, err := svc.Service.UpdateReportSchedule(ctx, session, cfg) - return svc.upsertAfterReportChange(ctx, report, err) -} - -func (svc atomService) EnableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) { - report, err := svc.Service.EnableReportConfig(ctx, session, id) - return svc.upsertAfterReportChange(ctx, report, err) -} - -func (svc atomService) DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) { - report, err := svc.Service.DisableReportConfig(ctx, session, id) - return svc.upsertAfterReportChange(ctx, report, err) -} - -func (svc atomService) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - if err := svc.Service.RemoveReportConfig(ctx, session, id); err != nil { - return err - } - _ = svc.projector.DeleteResource(ctx, id) - return nil -} - -func (svc atomService) upsertAfterReportChange(ctx context.Context, report ReportConfig, err error) (ReportConfig, error) { - if err != nil { - return report, err - } - if err := svc.projector.UpsertResource(ctx, reportProjection(report)); err != nil { - return report, nil - } - return report, nil -} - -func reportProjection(r ReportConfig) atom.Resource { - res := atom.ResourceFromFields(atom.ObjectFields{ - ID: r.ID, - Kind: atom.KindReport, - Name: r.Name, - TenantID: r.DomainID, - OwnerID: r.CreatedBy, - Status: r.Status.String(), - Metadata: map[string]any{"description": r.Description}, - CreatedBy: r.CreatedBy, - UpdatedBy: r.UpdatedBy, - CreatedAt: r.CreatedAt, - UpdatedAt: r.UpdatedAt, - Description: r.Description, - }) - res.Attributes["scheduled_at"] = r.Schedule.Time - return res -} diff --git a/reports/events/doc.go b/reports/events/doc.go deleted file mode 100644 index 999dbec24..000000000 --- a/reports/events/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package events provides the domain concept definitions needed to support -// reports events functionality. -package events diff --git a/reports/events/events.go b/reports/events/events.go deleted file mode 100644 index 6f3e506c7..000000000 --- a/reports/events/events.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/reports" -) - -const ( - reportPrefix = "report." - reportCreate = reportPrefix + "create" - reportRemove = reportPrefix + "remove" -) - -var ( - _ events.Event = (*createReportConfigEvent)(nil) - _ events.Event = (*removeReportConfigEvent)(nil) -) - -type baseReportEvent struct { - session authn.Session - requestID string -} - -func newBaseReportEvent(session authn.Session, requestID string) baseReportEvent { - return baseReportEvent{ - session: session, - requestID: requestID, - } -} - -func (bre baseReportEvent) Encode() map[string]any { - return map[string]any{ - "domain": bre.session.DomainID, - "user_id": bre.session.UserID, - "token_type": bre.session.Type.String(), - "super_admin": bre.session.SuperAdmin, - "request_id": bre.requestID, - } -} - -type createReportConfigEvent struct { - cfg reports.ReportConfig - baseReportEvent -} - -func (e createReportConfigEvent) Encode() (map[string]any, error) { - val := e.baseReportEvent.Encode() - val["id"] = e.cfg.ID - val["name"] = e.cfg.Name - val["operation"] = reportCreate - return val, nil -} - -type removeReportConfigEvent struct { - id string - baseReportEvent -} - -func (e removeReportConfigEvent) Encode() (map[string]any, error) { - val := e.baseReportEvent.Encode() - val["id"] = e.id - val["operation"] = reportRemove - return val, nil -} diff --git a/reports/events/streams.go b/reports/events/streams.go deleted file mode 100644 index 706387319..000000000 --- a/reports/events/streams.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/events/store" - "github.com/absmach/magistrala/reports" - "github.com/go-chi/chi/v5/middleware" -) - -const ( - magistralaPrefix = "magistrala." - CreateStream = magistralaPrefix + reportCreate - RemoveStream = magistralaPrefix + reportRemove -) - -var _ reports.Service = (*eventStore)(nil) - -type eventStore struct { - events.Publisher - svc reports.Service -} - -func NewEventStoreMiddleware(ctx context.Context, svc reports.Service, url string) (reports.Service, error) { - publisher, err := store.NewPublisher(ctx, url, "reports-es-pub") - if err != nil { - return nil, err - } - - return &eventStore{ - svc: svc, - Publisher: publisher, - }, nil -} - -func (es *eventStore) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - reportCfg, err := es.svc.AddReportConfig(ctx, session, cfg) - if err != nil { - return reportCfg, err - } - event := createReportConfigEvent{ - cfg: reportCfg, - baseReportEvent: newBaseReportEvent(session, middleware.GetReqID(ctx)), - } - if err := es.Publish(ctx, CreateStream, event); err != nil { - return reportCfg, err - } - return reportCfg, nil -} - -func (es *eventStore) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - if err := es.svc.RemoveReportConfig(ctx, session, id); err != nil { - return err - } - event := removeReportConfigEvent{ - id: id, - baseReportEvent: newBaseReportEvent(session, middleware.GetReqID(ctx)), - } - return es.Publish(ctx, RemoveStream, event) -} - -func (es *eventStore) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - return es.svc.ViewReportConfig(ctx, session, id, withRoles) -} - -func (es *eventStore) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - return es.svc.UpdateReportConfig(ctx, session, cfg) -} - -func (es *eventStore) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - return es.svc.UpdateReportSchedule(ctx, session, cfg) -} - -func (es *eventStore) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - return es.svc.ListReportsConfig(ctx, session, pm) -} - -func (es *eventStore) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - return es.svc.EnableReportConfig(ctx, session, id) -} - -func (es *eventStore) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - return es.svc.DisableReportConfig(ctx, session, id) -} - -func (es *eventStore) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - return es.svc.UpdateReportTemplate(ctx, session, cfg) -} - -func (es *eventStore) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - return es.svc.ViewReportTemplate(ctx, session, id) -} - -func (es *eventStore) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - return es.svc.DeleteReportTemplate(ctx, session, id) -} - -func (es *eventStore) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - return es.svc.GenerateReport(ctx, session, config, action) -} - -func (es *eventStore) StartScheduler(ctx context.Context) error { - return es.svc.StartScheduler(ctx) -} diff --git a/reports/events/streams_test.go b/reports/events/streams_test.go deleted file mode 100644 index 8c86d850f..000000000 --- a/reports/events/streams_test.go +++ /dev/null @@ -1,632 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/events" - "github.com/absmach/magistrala/reports/mocks" - "github.com/go-chi/chi/v5/middleware" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - storeClient *redis.Client - storeURL string - validSession = authn.Session{ - DomainID: testsutil.GenerateUUID(&testing.T{}), - UserID: testsutil.GenerateUUID(&testing.T{}), - } - validReportConfig = generateTestReportConfig(&testing.T{}) - validReportConfigPage = reports.ReportConfigPage{ - PageMeta: reports.PageMeta{ - Limit: 10, - Offset: 0, - Total: 1, - }, - ReportConfigs: []reports.ReportConfig{validReportConfig}, - } -) - -func newEventStoreMiddleware(t *testing.T) (*mocks.Service, reports.Service) { - svc := new(mocks.Service) - nsvc, err := events.NewEventStoreMiddleware(context.Background(), svc, storeURL) - require.Nil(t, err, fmt.Sprintf("create events store middleware failed with unexpected error: %s", err)) - - return svc, nsvc -} - -func TestMain(m *testing.M) { - code := testsutil.RunRedisTest(m, &storeClient, &storeURL) - os.Exit(code) -} - -func TestAddReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "publish successfully", - session: validSession, - cfg: validReportConfig, - svcRes: validReportConfig, - svcErr: nil, - resp: validReportConfig, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - cfg: validReportConfig, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrCreateEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("AddReportConfig", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.AddReportConfig(validCtx, tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestRemoveReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - svcErr error - err error - }{ - { - desc: "publish successfully", - session: validSession, - reportID: validReportConfig.ID, - svcErr: nil, - err: nil, - }, - { - desc: "failed to publish with service error", - session: validSession, - reportID: validReportConfig.ID, - svcErr: svcerr.ErrRemoveEntity, - err: svcerr.ErrRemoveEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("RemoveReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcErr) - err := nsvc.RemoveReportConfig(validCtx, tc.session, tc.reportID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestViewReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - withRoles bool - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "view successfully", - session: validSession, - reportID: validReportConfig.ID, - withRoles: false, - svcRes: validReportConfig, - svcErr: nil, - resp: validReportConfig, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - reportID: validReportConfig.ID, - withRoles: false, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrViewEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("ViewReportConfig", validCtx, tc.session, tc.reportID, tc.withRoles).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.ViewReportConfig(validCtx, tc.session, tc.reportID, tc.withRoles) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - updatedCfg := validReportConfig - updatedCfg.Name = "updatedName" - - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "update successfully", - session: validSession, - cfg: updatedCfg, - svcRes: updatedCfg, - svcErr: nil, - resp: updatedCfg, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - cfg: updatedCfg, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrUpdateEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateReportConfig", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.UpdateReportConfig(validCtx, tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateReportSchedule(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "update schedule successfully", - session: validSession, - cfg: validReportConfig, - svcRes: validReportConfig, - svcErr: nil, - resp: validReportConfig, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - cfg: validReportConfig, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrUpdateEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateReportSchedule", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.UpdateReportSchedule(validCtx, tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestListReportsConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - pageMeta reports.PageMeta - svcRes reports.ReportConfigPage - svcErr error - resp reports.ReportConfigPage - err error - }{ - { - desc: "list successfully", - session: validSession, - pageMeta: reports.PageMeta{ - Limit: 10, - Offset: 0, - }, - svcRes: validReportConfigPage, - svcErr: nil, - resp: validReportConfigPage, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - pageMeta: reports.PageMeta{ - Limit: 10, - Offset: 0, - }, - svcRes: reports.ReportConfigPage{}, - svcErr: svcerr.ErrViewEntity, - resp: reports.ReportConfigPage{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("ListReportsConfig", validCtx, tc.session, tc.pageMeta).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.ListReportsConfig(validCtx, tc.session, tc.pageMeta) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestEnableReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "enable successfully", - session: validSession, - reportID: validReportConfig.ID, - svcRes: validReportConfig, - svcErr: nil, - resp: validReportConfig, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - reportID: validReportConfig.ID, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrUpdateEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("EnableReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.EnableReportConfig(validCtx, tc.session, tc.reportID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestDisableReportConfig(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - svcRes reports.ReportConfig - svcErr error - resp reports.ReportConfig - err error - }{ - { - desc: "disable successfully", - session: validSession, - reportID: validReportConfig.ID, - svcRes: validReportConfig, - svcErr: nil, - resp: validReportConfig, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - reportID: validReportConfig.ID, - svcRes: reports.ReportConfig{}, - svcErr: svcerr.ErrUpdateEntity, - resp: reports.ReportConfig{}, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("DisableReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.DisableReportConfig(validCtx, tc.session, tc.reportID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestUpdateReportTemplate(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - svcErr error - err error - }{ - { - desc: "update template successfully", - session: validSession, - cfg: validReportConfig, - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - cfg: validReportConfig, - svcErr: svcerr.ErrUpdateEntity, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("UpdateReportTemplate", validCtx, tc.session, tc.cfg).Return(tc.svcErr) - err := nsvc.UpdateReportTemplate(validCtx, tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestViewReportTemplate(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - svcRes reports.ReportTemplate - svcErr error - resp reports.ReportTemplate - err error - }{ - { - desc: "view template successfully", - session: validSession, - reportID: validReportConfig.ID, - svcRes: reports.ReportTemplate("template content"), - svcErr: nil, - resp: reports.ReportTemplate("template content"), - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - reportID: validReportConfig.ID, - svcRes: reports.ReportTemplate(""), - svcErr: svcerr.ErrViewEntity, - resp: reports.ReportTemplate(""), - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("ViewReportTemplate", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.ViewReportTemplate(validCtx, tc.session, tc.reportID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestDeleteReportTemplate(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - reportID string - svcErr error - err error - }{ - { - desc: "delete template successfully", - session: validSession, - reportID: validReportConfig.ID, - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - reportID: validReportConfig.ID, - svcErr: svcerr.ErrRemoveEntity, - err: svcerr.ErrRemoveEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("DeleteReportTemplate", validCtx, tc.session, tc.reportID).Return(tc.svcErr) - err := nsvc.DeleteReportTemplate(validCtx, tc.session, tc.reportID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func TestGenerateReport(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) - - cases := []struct { - desc string - session authn.Session - config reports.ReportConfig - action reports.ReportAction - svcRes reports.ReportPage - svcErr error - resp reports.ReportPage - err error - }{ - { - desc: "generate report successfully", - session: validSession, - config: validReportConfig, - action: reports.ViewReport, - svcRes: reports.ReportPage{}, - svcErr: nil, - resp: reports.ReportPage{}, - err: nil, - }, - { - desc: "failed with service error", - session: validSession, - config: validReportConfig, - action: reports.ViewReport, - svcRes: reports.ReportPage{}, - svcErr: svcerr.ErrViewEntity, - resp: reports.ReportPage{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("GenerateReport", validCtx, tc.session, tc.config, tc.action).Return(tc.svcRes, tc.svcErr) - resp, err := nsvc.GenerateReport(validCtx, tc.session, tc.config, tc.action) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - svcCall.Unset() - }) - } -} - -func TestStartScheduler(t *testing.T) { - svc, nsvc := newEventStoreMiddleware(t) - - cases := []struct { - desc string - svcErr error - err error - }{ - { - desc: "start scheduler successfully", - svcErr: nil, - err: nil, - }, - { - desc: "failed with service error", - svcErr: svcerr.ErrCreateEntity, - err: svcerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("StartScheduler", context.Background()).Return(tc.svcErr) - err := nsvc.StartScheduler(context.Background()) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) - } -} - -func generateTestReportConfig(t *testing.T) reports.ReportConfig { - createdAt, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") - assert.Nil(t, err, fmt.Sprintf("Unexpected error parsing time: %v", err)) - return reports.ReportConfig{ - ID: testsutil.GenerateUUID(t), - Name: "testreport", - DomainID: testsutil.GenerateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: createdAt, - UpdatedAt: createdAt, - } -} diff --git a/reports/generator.go b/reports/generator.go deleted file mode 100644 index c5a829ee7..000000000 --- a/reports/generator.go +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "bytes" - "context" - "encoding/csv" - "fmt" - "html/template" - "io" - "log/slog" - "mime/multipart" - "net/http" - "sort" - "strings" - "time" - _ "time/tzdata" // Embed timezone database - - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/transformers/senml" -) - -const ( - nanosecondThreshold = float64(10 * time.Second / time.Nanosecond) - templateFormatTime = "formatTime" - templateFormatValue = "formatValue" -) - -type ReportData struct { - Title string - GeneratedTime string - GeneratedDate string - Reports []Report - Timezone string -} - -func (r *report) generatePDFReport(ctx context.Context, title string, reports []Report, template ReportTemplate, timezone string) ([]byte, error) { - for i := range reports { - sort.Slice(reports[i].Messages, func(j, k int) bool { - return reports[i].Messages[j].Time < reports[i].Messages[k].Time - }) - } - - loc, err := resolveTimezone(timezone) - if err != nil { - r.runInfo <- pkglog.RunInfo{ - Level: slog.LevelWarn, - Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err), - Details: []slog.Attr{ - slog.String("report_title", title), - slog.Time("time", time.Now().UTC()), - }, - } - } - - now := time.Now().In(loc) - displayTZ := timezone - if strings.TrimSpace(displayTZ) == "" { - displayTZ = "UTC" - } - - data := ReportData{ - Title: title, - GeneratedTime: now.Format("15:04:05"), - GeneratedDate: now.Format("02 Jan 2006"), - Reports: reports, - Timezone: displayTZ, - } - - templateContent := r.defaultTemplate.String() - if template.String() != "" { - templateContent = template.String() - } - return r.generate(ctx, templateContent, data) -} - -func (r *report) generate(ctx context.Context, templateContent string, data ReportData) ([]byte, error) { - tmpl := template.New("report").Funcs(template.FuncMap{ - templateFormatTime: func(t float64) string { return r.formatTimeWithTimezone(t, data.Timezone) }, - templateFormatValue: formatValue, - "add": func(a, b int) int { return a + b }, - "sub": func(a, b int) int { return a - b }, - "iterate": func(count int) []int { return makeRange(count) }, - "ge": func(a, b int) bool { return a >= b }, - "lt": func(a, b int) bool { return a < b }, - "eq": func(a, b int) bool { return a == b }, - "div": func(a, b int) int { - if b == 0 { - return 0 - } - return a / b - }, - "mod": func(a, b int) int { - if b == 0 { - return 0 - } - return a % b - }, - "getStartRow": getStartRow, - "getEndRow": getEndRow, - }) - - tmpl, err := tmpl.Parse(templateContent) - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - var htmlBuf bytes.Buffer - if err := tmpl.Execute(&htmlBuf, data); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - htmlContent := htmlBuf.String() - pdfBytes, err := r.htmlToPDF(ctx, htmlContent) - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return pdfBytes, nil -} - -func (r *report) htmlToPDF(ctx context.Context, htmlContent string) ([]byte, error) { - var requestBody bytes.Buffer - writer := multipart.NewWriter(&requestBody) - - htmlPart, err := writer.CreateFormFile("files", "index.html") - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if _, err := htmlPart.Write([]byte(htmlContent)); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.WriteField("marginTop", "0"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.WriteField("marginBottom", "0"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.WriteField("marginLeft", "0"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.WriteField("marginRight", "0"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.WriteField("printBackground", "true"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.WriteField("preferCSSPageSize", "true"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.WriteField("emulatedMediaType", "print"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.WriteField("waitForSelector", "body"); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.Close(); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, &requestBody) - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - req.Header.Set("Content-Type", writer.FormDataContentType()) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - defer resp.Body.Close() - - pdfBytes, err := io.ReadAll(resp.Body) - if err != nil || resp.StatusCode != http.StatusOK { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - return pdfBytes, nil -} - -func (r *report) formatTimeWithTimezone(t float64, timezone string) string { - loc, err := resolveTimezone(timezone) - if err != nil { - r.runInfo <- pkglog.RunInfo{ - Level: slog.LevelWarn, - Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err), - Details: []slog.Attr{slog.Time("time", time.Now().UTC())}, - } - } - - var timeVal time.Time - switch { - case t > nanosecondThreshold: - timeVal = time.Unix(0, int64(t)).In(loc) - default: - timeVal = time.Unix(int64(t), 0).In(loc) - } - - return timeVal.Format("2006-01-02 15:04:05") -} - -func formatValue(msg senml.Message) string { - switch { - case msg.Value != nil: - return fmt.Sprintf("%.2f", *msg.Value) - case msg.StringValue != nil: - return *msg.StringValue - case msg.BoolValue != nil: - return fmt.Sprintf("%t", *msg.BoolValue) - case msg.DataValue != nil: - return *msg.DataValue - default: - return "N/A" - } -} - -func makeRange(n int) []int { - result := make([]int, n) - for i := range result { - result[i] = i - } - return result -} - -func getStartRow(pageNum, firstPageRows, continuationPageRows int) int { - if pageNum == 0 { - return 0 - } - return firstPageRows + (pageNum-1)*continuationPageRows -} - -func getEndRow(pageNum, firstPageRows, continuationPageRows, totalMessages int) int { - var end int - if pageNum == 0 { - end = firstPageRows - } else { - start := firstPageRows + (pageNum-1)*continuationPageRows - end = start + continuationPageRows - } - - if end > totalMessages { - end = totalMessages - } - return end -} - -func (r *report) generateCSVReport(_ context.Context, title string, reports []Report, timezone string) ([]byte, error) { - var buf bytes.Buffer - writer := csv.NewWriter(&buf) - - headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"} - - for i, report := range reports { - if i > 0 { - if err := writer.Write([]string{""}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.Write([]string{""}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - } else { - if err := writer.Write([]string{title}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.Write([]string{""}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - } - - if err := writer.Write([]string{"Report Information:"}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.Write([]string{"Name", report.Metric.Name}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if report.Metric.ClientID != "" { - if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - } - if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - if err := writer.Write([]string{""}); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - if err := writer.Write(headers); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - sort.Slice(report.Messages, func(i, j int) bool { - return report.Messages[i].Time < report.Messages[j].Time - }) - - for _, msg := range report.Messages { - timeStr := r.formatTimeWithTimezone(msg.Time, timezone) - - var valueStr string - if msg.Value != nil { - valueStr = fmt.Sprintf("%.2f", *msg.Value) - } else if msg.StringValue != nil { - valueStr = *msg.StringValue - } else if msg.BoolValue != nil { - valueStr = fmt.Sprintf("%v", *msg.BoolValue) - } else if msg.DataValue != nil { - valueStr = *msg.DataValue - } else { - valueStr = "N/A" - } - - row := []string{ - timeStr, - valueStr, - msg.Unit, - msg.Protocol, - msg.Subtopic, - } - - if err := writer.Write(row); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - } - } - - writer.Flush() - if err := writer.Error(); err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return buf.Bytes(), nil -} diff --git a/reports/handler.go b/reports/handler.go deleted file mode 100644 index e97a5042e..000000000 --- a/reports/handler.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "context" - "fmt" - "log/slog" - "time" - - pkglog "github.com/absmach/magistrala/pkg/logger" -) - -func (r *report) StartScheduler(ctx context.Context) error { - defer r.ticker.Stop() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-r.ticker.Tick(): - due := time.Now().UTC() - - pm := PageMeta{ - Status: EnabledStatus, - ScheduledBefore: &due, - } - - reportConfigs, err := r.repo.ListAllReportsConfig(ctx, pm) - if err != nil { - r.runInfo <- pkglog.RunInfo{ - Level: slog.LevelError, - Message: fmt.Sprintf("failed to list reports : %s", err), - Details: []slog.Attr{slog.Time("due", due)}, - } - continue - } - - for _, c := range reportConfigs.ReportConfigs { - go func(cfg ReportConfig) { - if _, err := r.repo.UpdateReportDue(ctx, cfg.ID, cfg.Schedule.NextDue()); err != nil { - r.runInfo <- pkglog.RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to update report: %s", err), Details: []slog.Attr{slog.Time("time", time.Now().UTC())}} - return - } - _, err := r.generateReport(ctx, cfg, EmailReport) - ret := pkglog.RunInfo{ - Details: []slog.Attr{ - slog.String("domain_id", cfg.DomainID), - slog.String("report_id", cfg.ID), - slog.String("report_name", cfg.Name), - slog.Time("exec_time", time.Now().UTC()), - }, - } - if err != nil { - ret.Level = slog.LevelError - ret.Message = fmt.Sprintf("failed to generate report: %s", err) - } - r.runInfo <- ret - }(c) - } - } - } -} diff --git a/reports/middleware/authorization.go b/reports/middleware/authorization.go deleted file mode 100644 index 620a08ef4..000000000 --- a/reports/middleware/authorization.go +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - smqauthz "github.com/absmach/magistrala/pkg/authz" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/policies" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/operations" -) - -var ( - errDomainCreateConfigs = errors.New("not authorized to create report configs in domain") - errDomainViewConfigs = errors.New("not authorized to view report configs in domain") - errDomainUpdateConfigs = errors.New("not authorized to update report configs in domain") - errDomainDeleteConfigs = errors.New("not authorized to delete report configs in domain") - errDomainGenerateReports = errors.New("not authorized to generate reports in domain") - - errDomainUpdateTemplates = errors.New("not authorized to update report templates in domain") - errDomainRemoveTemplates = errors.New("not authorized to delete report templates in domain") - errDomainViewTemplates = errors.New("not authorized to view report templates in domain") -) - -type authorizationMiddleware struct { - svc reports.Service - authz smqauthz.Authorization - atomAuthz atom.Authorizer - entitiesOps permissions.EntitiesOperations[permissions.Operation] -} - -// AuthorizationMiddleware adds authorization to the reports service. -func AuthorizationMiddleware(svc reports.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (reports.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - return &authorizationMiddleware{ - svc: svc, - authz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func AtomAuthorizationMiddleware(svc reports.Service, authz atom.Authorizer, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (reports.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - return &authorizationMiddleware{ - svc: svc, - atomAuthz: authz, - entitiesOps: entitiesOps, - }, nil -} - -func (am *authorizationMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpAddReportConfig, session, policies.DomainType, session.DomainID); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainCreateConfigs, err) - } - - return am.svc.AddReportConfig(ctx, session, cfg) -} - -func (am *authorizationMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpViewReportConfig, session, operations.EntityType, id); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainViewConfigs, err) - } - - return am.svc.ViewReportConfig(ctx, session, id, withRoles) -} - -func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpUpdateReportConfig, session, operations.EntityType, cfg.ID); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.UpdateReportConfig(ctx, session, cfg) -} - -func (am *authorizationMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpUpdateReportSchedule, session, operations.EntityType, cfg.ID); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.UpdateReportSchedule(ctx, session, cfg) -} - -func (am *authorizationMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - if err := am.authorize(ctx, operations.OpRemoveReportConfig, session, operations.EntityType, id); err != nil { - return errors.Wrap(errDomainDeleteConfigs, err) - } - - return am.svc.RemoveReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - switch err := am.checkSuperAdmin(ctx, session); { - case err == nil: - session.SuperAdmin = true - case errors.Contains(err, svcerr.ErrSuperAdminAction): - if err := am.authorize(ctx, operations.OpListReportsConfig, session, operations.EntityType, auth.AnyIDs); err != nil { - return reports.ReportConfigPage{}, errors.Wrap(errDomainViewConfigs, err) - } - default: - return reports.ReportConfigPage{}, err - } - - return am.svc.ListReportsConfig(ctx, session, pm) -} - -func (am *authorizationMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpEnableReportConfig, session, operations.EntityType, id); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.EnableReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - if err := am.authorize(ctx, operations.OpDisableReportConfig, session, operations.EntityType, id); err != nil { - return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.DisableReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - if err := am.authorize(ctx, operations.OpGenerateReport, session, policies.DomainType, session.DomainID); err != nil { - return reports.ReportPage{}, errors.Wrap(errDomainGenerateReports, err) - } - - return am.svc.GenerateReport(ctx, session, config, action) -} - -func (am *authorizationMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - if err := am.authorize(ctx, operations.OpUpdateReportTemplate, session, operations.EntityType, cfg.ID); err != nil { - return errors.Wrap(errDomainUpdateTemplates, err) - } - - return am.svc.UpdateReportTemplate(ctx, session, cfg) -} - -func (am *authorizationMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - if err := am.authorize(ctx, operations.OpViewReportTemplate, session, operations.EntityType, id); err != nil { - return "", errors.Wrap(errDomainViewTemplates, err) - } - - return am.svc.ViewReportTemplate(ctx, session, id) -} - -func (am *authorizationMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - if err := am.authorize(ctx, operations.OpDeleteReportTemplate, session, operations.EntityType, id); err != nil { - return errors.Wrap(errDomainRemoveTemplates, err) - } - - return am.svc.DeleteReportTemplate(ctx, session, id) -} - -func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error { - return am.svc.StartScheduler(ctx) -} - -func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error { - perm, err := am.entitiesOps.GetPermission(operations.EntityType, op) - if err != nil { - return err - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, perm.String(), objType, obj, atom.KindReport) - } - - pr := smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: obj, - ObjectType: objType, - Permission: perm.String(), - } - - var pat *smqauthz.PATReq - if session.PatID != "" { - entityID := obj - if objType == policies.DomainType { - entityID = auth.AnyIDs - } - opName := am.entitiesOps.OperationName(operations.EntityType, op) - pat = &smqauthz.PATReq{ - UserID: session.UserID, - PatID: session.PatID, - EntityID: entityID, - EntityType: auth.ReportsType.String(), - Operation: opName, - Domain: session.DomainID, - } - } - - if err := am.authz.Authorize(ctx, pr, pat); err != nil { - return err - } - - return nil -} - -func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { - if session.Role != authn.SuperAdminRole { - return svcerr.ErrSuperAdminAction - } - if am.atomAuthz != nil { - return atom.Authorize(ctx, am.atomAuthz, session, policies.AdminPermission, policies.PlatformType, policies.MagistralaObject, policies.PlatformType) - } - if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{ - SubjectType: policies.UserType, - Subject: session.UserID, - Permission: policies.AdminPermission, - ObjectType: policies.PlatformType, - Object: policies.MagistralaObject, - }, nil); err != nil { - return err - } - return nil -} diff --git a/reports/middleware/authorization_test.go b/reports/middleware/authorization_test.go deleted file mode 100644 index 74b7e08bd..000000000 --- a/reports/middleware/authorization_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "testing" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/atom" - "github.com/absmach/magistrala/pkg/authn" - pkgerrors "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/mocks" - "github.com/absmach/magistrala/reports/operations" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type recordingAtomAuthorizer struct { - allowed bool - reqs []atom.AuthzRequest -} - -func (a *recordingAtomAuthorizer) CheckAuthz(_ context.Context, req atom.AuthzRequest) (atom.AuthzResponse, error) { - a.reqs = append(a.reqs, req) - return atom.AuthzResponse{Allowed: a.allowed}, nil -} - -func TestListReportsConfigAuthorizesRegularUser(t *testing.T) { - svc := mocks.NewService(t) - pm := reports.PageMeta{Limit: 10} - session := authn.Session{UserID: "user-1", DomainID: "domain-1", DomainUserID: "domain-1_user-1"} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListReportsConfig", mock.Anything, session, pm).Return(reports.ReportConfigPage{PageMeta: reports.PageMeta{Limit: 10}}, nil).Once() - page, err := wrapped.ListReportsConfig(context.Background(), session, pm) - - require.NoError(t, err) - assert.Equal(t, uint64(10), page.Limit) - require.Len(t, authz.reqs, 1) - assert.Equal(t, atom.AuthzRequest{ - SubjectID: "user-1", - Action: "list", - ResourceID: auth.AnyIDs, - ObjectKind: "resource", - ObjectID: auth.AnyIDs, - Context: map[string]any{ - "domain_id": "domain-1", - "legacy_object_type": operations.EntityType, - }, - }, authz.reqs[0]) -} - -func TestListReportsConfigDeniedRegularUserDoesNotDelegate(t *testing.T) { - svc := mocks.NewService(t) - authz := &recordingAtomAuthorizer{allowed: false} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - _, err = wrapped.ListReportsConfig(context.Background(), authn.Session{UserID: "user-1", DomainID: "domain-1"}, reports.PageMeta{}) - - assert.True(t, pkgerrors.Contains(err, pkgerrors.ErrAuthorization)) - require.Len(t, authz.reqs, 1) -} - -func TestListReportsConfigSuperAdminSkipsListAuthorization(t *testing.T) { - svc := mocks.NewService(t) - pm := reports.PageMeta{Limit: 10} - session := authn.Session{UserID: "admin-1", DomainID: "domain-1", Role: authn.SuperAdminRole} - authz := &recordingAtomAuthorizer{allowed: true} - wrapped, err := AtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t)) - require.NoError(t, err) - - svc.On("ListReportsConfig", mock.Anything, mock.MatchedBy(func(s authn.Session) bool { - return s.SuperAdmin - }), pm).Return(reports.ReportConfigPage{PageMeta: reports.PageMeta{Limit: 10}}, nil).Once() - _, err = wrapped.ListReportsConfig(context.Background(), session, pm) - - require.NoError(t, err) - require.Len(t, authz.reqs, 1) - assert.Equal(t, "manage", authz.reqs[0].Action) -} - -func testEntitiesOps(t *testing.T) permissions.EntitiesOperations[permissions.Operation] { - t.Helper() - details := operations.OperationDetails() - perms := make(map[string]permissions.Permission, len(details)) - for _, detail := range details { - if detail.PermissionRequired { - perms[detail.Name] = permissions.Permission(detail.Name) - } - } - entitiesOps, err := permissions.NewEntitiesOperations( - permissions.EntitiesPermission{operations.EntityType: perms}, - permissions.EntitiesOperationDetails[permissions.Operation]{operations.EntityType: details}, - ) - require.NoError(t, err) - return entitiesOps -} diff --git a/reports/middleware/callout.go b/reports/middleware/callout.go deleted file mode 100644 index c3332b77b..000000000 --- a/reports/middleware/callout.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/callout" - "github.com/absmach/magistrala/pkg/permissions" - "github.com/absmach/magistrala/pkg/policies" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/operations" -) - -var _ reports.Service = (*calloutMiddleware)(nil) - -type calloutMiddleware struct { - svc reports.Service - callout callout.Callout - entitiesOps permissions.EntitiesOperations[permissions.Operation] -} - -const ( - entityIDKey = "entity_id" - entityType = "report" -) - -func NewCallout(svc reports.Service, callout callout.Callout, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (reports.Service, error) { - if err := entitiesOps.Validate(); err != nil { - return nil, err - } - - return &calloutMiddleware{ - svc: svc, - callout: callout, - entitiesOps: entitiesOps, - }, nil -} - -func (cm *calloutMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - params := map[string]any{ - "entities": cfg, - "count": 1, - } - - if err := cm.callOut(ctx, session, operations.OpAddReportConfig, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.AddReportConfig(ctx, session, cfg) -} - -func (cm *calloutMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpViewReportConfig, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.ViewReportConfig(ctx, session, id, withRoles) -} - -func (cm *calloutMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - params := map[string]any{ - entityIDKey: cfg.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateReportConfig, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.UpdateReportConfig(ctx, session, cfg) -} - -func (cm *calloutMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - params := map[string]any{ - entityIDKey: cfg.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateReportSchedule, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.UpdateReportSchedule(ctx, session, cfg) -} - -func (cm *calloutMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpRemoveReportConfig, params); err != nil { - return err - } - - return cm.svc.RemoveReportConfig(ctx, session, id) -} - -func (cm *calloutMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - params := map[string]any{ - "pagemeta": pm, - } - - if err := cm.callOut(ctx, session, operations.OpListReportsConfig, params); err != nil { - return reports.ReportConfigPage{}, err - } - - return cm.svc.ListReportsConfig(ctx, session, pm) -} - -func (cm *calloutMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpEnableReportConfig, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.EnableReportConfig(ctx, session, id) -} - -func (cm *calloutMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpDisableReportConfig, params); err != nil { - return reports.ReportConfig{}, err - } - - return cm.svc.DisableReportConfig(ctx, session, id) -} - -func (cm *calloutMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - params := map[string]any{ - entityIDKey: config.ID, - } - - if err := cm.callOut(ctx, session, operations.OpGenerateReport, params); err != nil { - return reports.ReportPage{}, err - } - - return cm.svc.GenerateReport(ctx, session, config, action) -} - -func (cm *calloutMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - params := map[string]any{ - entityIDKey: cfg.ID, - } - - if err := cm.callOut(ctx, session, operations.OpUpdateReportTemplate, params); err != nil { - return err - } - - return cm.svc.UpdateReportTemplate(ctx, session, cfg) -} - -func (cm *calloutMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpViewReportTemplate, params); err != nil { - return "", err - } - - return cm.svc.ViewReportTemplate(ctx, session, id) -} - -func (cm *calloutMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - params := map[string]any{ - entityIDKey: id, - } - - if err := cm.callOut(ctx, session, operations.OpDeleteReportTemplate, params); err != nil { - return err - } - - return cm.svc.DeleteReportTemplate(ctx, session, id) -} - -func (cm *calloutMiddleware) StartScheduler(ctx context.Context) error { - return cm.svc.StartScheduler(ctx) -} - -func (cm *calloutMiddleware) callOut(ctx context.Context, session authn.Session, op permissions.Operation, pld map[string]any) error { - var entityID string - if id, ok := pld[entityIDKey].(string); ok { - entityID = id - } - - req := callout.Request{ - BaseRequest: callout.BaseRequest{ - Operation: cm.entitiesOps.OperationName(entityType, op), - EntityType: entityType, - EntityID: entityID, - CallerID: session.UserID, - CallerType: policies.UserType, - DomainID: session.DomainID, - Time: time.Now().UTC(), - }, - Payload: pld, - } - - if err := cm.callout.Callout(ctx, req); err != nil { - return err - } - - return nil -} diff --git a/reports/middleware/logging.go b/reports/middleware/logging.go deleted file mode 100644 index 389f7463a..000000000 --- a/reports/middleware/logging.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "log/slog" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/reports" -) - -var _ reports.Service = (*loggingMiddleware)(nil) - -type loggingMiddleware struct { - logger *slog.Logger - svc reports.Service -} - -func LoggingMiddleware(svc reports.Service, logger *slog.Logger) reports.Service { - return &loggingMiddleware{ - logger: logger, - svc: svc, - } -} - -func (lm *loggingMiddleware) StartScheduler(ctx context.Context) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Start scheduler failed", args...) - return - } - lm.logger.Info("Start scheduler completed successfully", args...) - }(time.Now()) - return lm.svc.StartScheduler(ctx) -} - -func (lm *loggingMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (page reports.ReportPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Generate report failed", args...) - return - } - lm.logger.Info("Generate report completed", args...) - }(time.Now()) - - return lm.svc.GenerateReport(ctx, session, config, action) -} - -func (lm *loggingMiddleware) AddReportConfig(ctx context.Context, session authn.Session, config reports.ReportConfig) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("report_name", config.Name), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Add report config failed", args...) - return - } - lm.logger.Info("Add report config completed successfully", args...) - }(time.Now()) - return lm.svc.AddReportConfig(ctx, session, config) -} - -func (lm *loggingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("report_config", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("View report config failed", args...) - return - } - lm.logger.Info("View report config completed successfully", args...) - }(time.Now()) - return lm.svc.ViewReportConfig(ctx, session, id, withRoles) -} - -func (lm *loggingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, config reports.ReportConfig) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("report_config", - slog.String("id", config.ID), - slog.String("name", config.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update report config failed", args...) - return - } - lm.logger.Info("Update report config completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateReportConfig(ctx, session, config) -} - -func (lm *loggingMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("report", - slog.String("id", cfg.ID), - slog.Any("schedule", cfg.Schedule), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update report schedule failed", args...) - return - } - lm.logger.Info("Update report schedule completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateReportSchedule(ctx, session, cfg) -} - -func (lm *loggingMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (pg reports.ReportConfigPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("page", - slog.Uint64("offset", pm.Offset), - slog.Uint64("limit", pm.Limit), - slog.Uint64("total", pg.Total), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("List reports config failed", args...) - return - } - lm.logger.Info("List reports config completed successfully", args...) - }(time.Now()) - return lm.svc.ListReportsConfig(ctx, session, pm) -} - -func (lm *loggingMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("report_config", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Disable report config failed", args...) - return - } - lm.logger.Info("Disable report config completed successfully", args...) - }(time.Now()) - return lm.svc.DisableReportConfig(ctx, session, id) -} - -func (lm *loggingMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.Group("report_config", - slog.String("id", res.ID), - slog.String("name", res.Name), - ), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Enable report config failed", args...) - return - } - lm.logger.Info("Enable report config completed successfully", args...) - }(time.Now()) - return lm.svc.EnableReportConfig(ctx, session, id) -} - -func (lm *loggingMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("report_config_id", id), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Remove report config failed", args...) - return - } - lm.logger.Info("Remove report config completed successfully", args...) - }(time.Now()) - return lm.svc.RemoveReportConfig(ctx, session, id) -} - -func (lm *loggingMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("report_config_id", cfg.ID), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Update report template failed", args...) - return - } - lm.logger.Info("Update report template completed successfully", args...) - }(time.Now()) - - return lm.svc.UpdateReportTemplate(ctx, session, cfg) -} - -func (lm *loggingMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (t reports.ReportTemplate, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("report_config_id", id), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("View report template failed", args...) - return - } - lm.logger.Info("View report template completed successfully", args...) - }(time.Now()) - - return lm.svc.ViewReportTemplate(ctx, session, id) -} - -func (lm *loggingMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", session.DomainID), - slog.String("report_config_id", id), - } - if err != nil { - args = append(args, slog.String("error", err.Error())) - lm.logger.Warn("Delete report template failed", args...) - return - } - lm.logger.Info("Delete report template completed successfully", args...) - }(time.Now()) - - return lm.svc.DeleteReportTemplate(ctx, session, id) -} diff --git a/reports/middleware/metrics.go b/reports/middleware/metrics.go deleted file mode 100644 index 9e5fc23de..000000000 --- a/reports/middleware/metrics.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/reports" - "github.com/go-kit/kit/metrics" -) - -type metricsMiddleware struct { - counter metrics.Counter - latency metrics.Histogram - service reports.Service -} - -var _ reports.Service = (*metricsMiddleware)(nil) - -func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service reports.Service) reports.Service { - return &metricsMiddleware{ - counter: counter, - latency: latency, - service: service, - } -} - -func (mm *metricsMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "add_report_config").Add(1) - mm.latency.With("method", "add_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.AddReportConfig(ctx, session, cfg) -} - -func (mm *metricsMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "view_report_config").Add(1) - mm.latency.With("method", "view_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ViewReportConfig(ctx, session, id, withRoles) -} - -func (mm *metricsMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_report_config").Add(1) - mm.latency.With("method", "update_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateReportConfig(ctx, session, cfg) -} - -func (mm *metricsMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "update_report_schedule").Add(1) - mm.latency.With("method", "update_report_schedule").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateReportSchedule(ctx, session, cfg) -} - -func (mm *metricsMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - defer func(begin time.Time) { - mm.counter.With("method", "remove_report_config").Add(1) - mm.latency.With("method", "remove_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.RemoveReportConfig(ctx, session, id) -} - -func (mm *metricsMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - defer func(begin time.Time) { - mm.counter.With("method", "list_reports_config").Add(1) - mm.latency.With("method", "list_reports_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ListReportsConfig(ctx, session, pm) -} - -func (mm *metricsMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "enable_report_config").Add(1) - mm.latency.With("method", "enable_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.EnableReportConfig(ctx, session, id) -} - -func (mm *metricsMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - defer func(begin time.Time) { - mm.counter.With("method", "disable_report_config").Add(1) - mm.latency.With("method", "disable_report_config").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.DisableReportConfig(ctx, session, id) -} - -func (mm *metricsMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - defer func(begin time.Time) { - mm.counter.With("method", "update_report_template").Add(1) - mm.latency.With("method", "update_report_template").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.UpdateReportTemplate(ctx, session, cfg) -} - -func (mm *metricsMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - defer func(begin time.Time) { - mm.counter.With("method", "view_report_template").Add(1) - mm.latency.With("method", "view_report_template").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.ViewReportTemplate(ctx, session, id) -} - -func (mm *metricsMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - defer func(begin time.Time) { - mm.counter.With("method", "delete_report_template").Add(1) - mm.latency.With("method", "delete_report_template").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.DeleteReportTemplate(ctx, session, id) -} - -func (mm *metricsMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - defer func(begin time.Time) { - mm.counter.With("method", "generate_report").Add(1) - mm.latency.With("method", "generate_report").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.GenerateReport(ctx, session, config, action) -} - -func (mm *metricsMiddleware) StartScheduler(ctx context.Context) error { - defer func(begin time.Time) { - mm.counter.With("method", "start_scheduler").Add(1) - mm.latency.With("method", "start_scheduler").Observe(time.Since(begin).Seconds()) - }(time.Now()) - - return mm.service.StartScheduler(ctx) -} diff --git a/reports/middleware/tracing.go b/reports/middleware/tracing.go deleted file mode 100644 index c289a5ee1..000000000 --- a/reports/middleware/tracing.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - smqTracing "github.com/absmach/magistrala/pkg/tracing" - "github.com/absmach/magistrala/reports" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type tracingMiddleware struct { - tracer trace.Tracer - svc reports.Service -} - -var _ reports.Service = (*tracingMiddleware)(nil) - -func NewTracingMiddleware(tracer trace.Tracer, svc reports.Service) reports.Service { - return &tracingMiddleware{ - tracer: tracer, - svc: svc, - } -} - -func (tm *tracingMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "add_report_config", trace.WithAttributes( - attribute.String("name", cfg.Name), - attribute.String("domain_id", cfg.DomainID), - )) - defer span.End() - - return tm.svc.AddReportConfig(ctx, session, cfg) -} - -func (tm *tracingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "view_report_config", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.ViewReportConfig(ctx, session, id, withRoles) -} - -func (tm *tracingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_config", trace.WithAttributes( - attribute.String("id", cfg.ID), - )) - defer span.End() - - return tm.svc.UpdateReportConfig(ctx, session, cfg) -} - -func (tm *tracingMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_schedule", trace.WithAttributes( - attribute.String("id", cfg.ID), - )) - defer span.End() - - return tm.svc.UpdateReportSchedule(ctx, session, cfg) -} - -func (tm *tracingMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "remove_report_config", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.RemoveReportConfig(ctx, session, id) -} - -func (tm *tracingMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_reports_config", trace.WithAttributes( - attribute.Int("offset", int(pm.Offset)), - attribute.Int("limit", int(pm.Limit)), - )) - defer span.End() - - return tm.svc.ListReportsConfig(ctx, session, pm) -} - -func (tm *tracingMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "enable_report_config", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.EnableReportConfig(ctx, session, id) -} - -func (tm *tracingMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "disable_report_config", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.DisableReportConfig(ctx, session, id) -} - -func (tm *tracingMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_template", trace.WithAttributes( - attribute.String("id", cfg.ID), - )) - defer span.End() - - return tm.svc.UpdateReportTemplate(ctx, session, cfg) -} - -func (tm *tracingMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "view_report_template", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.ViewReportTemplate(ctx, session, id) -} - -func (tm *tracingMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "delete_report_template", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - - return tm.svc.DeleteReportTemplate(ctx, session, id) -} - -func (tm *tracingMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "generate_report", trace.WithAttributes( - attribute.String("config_id", config.ID), - attribute.String("action", string(action)), - )) - defer span.End() - - return tm.svc.GenerateReport(ctx, session, config, action) -} - -func (tm *tracingMiddleware) StartScheduler(ctx context.Context) error { - ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "start_scheduler") - defer span.End() - - return tm.svc.StartScheduler(ctx) -} diff --git a/reports/mocks/repository.go b/reports/mocks/repository.go deleted file mode 100644 index 4cd1e142d..000000000 --- a/reports/mocks/repository.go +++ /dev/null @@ -1,773 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - "time" - - "github.com/absmach/magistrala/reports" - mock "github.com/stretchr/testify/mock" -) - -// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -type Repository_Expecter struct { - mock *mock.Mock -} - -func (_m *Repository) EXPECT() *Repository_Expecter { - return &Repository_Expecter{mock: &_m.Mock} -} - -// AddReportConfig provides a mock function for the type Repository -func (_mock *Repository) AddReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for AddReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_AddReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddReportConfig' -type Repository_AddReportConfig_Call struct { - *mock.Call -} - -// AddReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - cfg reports.ReportConfig -func (_e *Repository_Expecter) AddReportConfig(ctx interface{}, cfg interface{}) *Repository_AddReportConfig_Call { - return &Repository_AddReportConfig_Call{Call: _e.mock.On("AddReportConfig", ctx, cfg)} -} - -func (_c *Repository_AddReportConfig_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_AddReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 reports.ReportConfig - if args[1] != nil { - arg1 = args[1].(reports.ReportConfig) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_AddReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_AddReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_AddReportConfig_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_AddReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// DeleteReportTemplate provides a mock function for the type Repository -func (_mock *Repository) DeleteReportTemplate(ctx context.Context, domainID string, reportID string) error { - ret := _mock.Called(ctx, domainID, reportID) - - if len(ret) == 0 { - panic("no return value specified for DeleteReportTemplate") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = returnFunc(ctx, domainID, reportID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_DeleteReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteReportTemplate' -type Repository_DeleteReportTemplate_Call struct { - *mock.Call -} - -// DeleteReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - domainID string -// - reportID string -func (_e *Repository_Expecter) DeleteReportTemplate(ctx interface{}, domainID interface{}, reportID interface{}) *Repository_DeleteReportTemplate_Call { - return &Repository_DeleteReportTemplate_Call{Call: _e.mock.On("DeleteReportTemplate", ctx, domainID, reportID)} -} - -func (_c *Repository_DeleteReportTemplate_Call) Run(run func(ctx context.Context, domainID string, reportID string)) *Repository_DeleteReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_DeleteReportTemplate_Call) Return(err error) *Repository_DeleteReportTemplate_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_DeleteReportTemplate_Call) RunAndReturn(run func(ctx context.Context, domainID string, reportID string) error) *Repository_DeleteReportTemplate_Call { - _c.Call.Return(run) - return _c -} - -// ListAllReportsConfig provides a mock function for the type Repository -func (_mock *Repository) ListAllReportsConfig(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error) { - ret := _mock.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for ListAllReportsConfig") - } - - var r0 reports.ReportConfigPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.PageMeta) (reports.ReportConfigPage, error)); ok { - return returnFunc(ctx, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.PageMeta) reports.ReportConfigPage); ok { - r0 = returnFunc(ctx, pm) - } else { - r0 = ret.Get(0).(reports.ReportConfigPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, reports.PageMeta) error); ok { - r1 = returnFunc(ctx, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ListAllReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllReportsConfig' -type Repository_ListAllReportsConfig_Call struct { - *mock.Call -} - -// ListAllReportsConfig is a helper method to define mock.On call -// - ctx context.Context -// - pm reports.PageMeta -func (_e *Repository_Expecter) ListAllReportsConfig(ctx interface{}, pm interface{}) *Repository_ListAllReportsConfig_Call { - return &Repository_ListAllReportsConfig_Call{Call: _e.mock.On("ListAllReportsConfig", ctx, pm)} -} - -func (_c *Repository_ListAllReportsConfig_Call) Run(run func(ctx context.Context, pm reports.PageMeta)) *Repository_ListAllReportsConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 reports.PageMeta - if args[1] != nil { - arg1 = args[1].(reports.PageMeta) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_ListAllReportsConfig_Call) Return(reportConfigPage reports.ReportConfigPage, err error) *Repository_ListAllReportsConfig_Call { - _c.Call.Return(reportConfigPage, err) - return _c -} - -func (_c *Repository_ListAllReportsConfig_Call) RunAndReturn(run func(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error)) *Repository_ListAllReportsConfig_Call { - _c.Call.Return(run) - return _c -} - -// RemoveReportConfig provides a mock function for the type Repository -func (_mock *Repository) RemoveReportConfig(ctx context.Context, id string) error { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for RemoveReportConfig") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_RemoveReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReportConfig' -type Repository_RemoveReportConfig_Call struct { - *mock.Call -} - -// RemoveReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *Repository_Expecter) RemoveReportConfig(ctx interface{}, id interface{}) *Repository_RemoveReportConfig_Call { - return &Repository_RemoveReportConfig_Call{Call: _e.mock.On("RemoveReportConfig", ctx, id)} -} - -func (_c *Repository_RemoveReportConfig_Call) Run(run func(ctx context.Context, id string)) *Repository_RemoveReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_RemoveReportConfig_Call) Return(err error) *Repository_RemoveReportConfig_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_RemoveReportConfig_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_RemoveReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportConfig provides a mock function for the type Repository -func (_mock *Repository) UpdateReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfig' -type Repository_UpdateReportConfig_Call struct { - *mock.Call -} - -// UpdateReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - cfg reports.ReportConfig -func (_e *Repository_Expecter) UpdateReportConfig(ctx interface{}, cfg interface{}) *Repository_UpdateReportConfig_Call { - return &Repository_UpdateReportConfig_Call{Call: _e.mock.On("UpdateReportConfig", ctx, cfg)} -} - -func (_c *Repository_UpdateReportConfig_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 reports.ReportConfig - if args[1] != nil { - arg1 = args[1].(reports.ReportConfig) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_UpdateReportConfig_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportConfigStatus provides a mock function for the type Repository -func (_mock *Repository) UpdateReportConfigStatus(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfigStatus") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateReportConfigStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfigStatus' -type Repository_UpdateReportConfigStatus_Call struct { - *mock.Call -} - -// UpdateReportConfigStatus is a helper method to define mock.On call -// - ctx context.Context -// - cfg reports.ReportConfig -func (_e *Repository_Expecter) UpdateReportConfigStatus(ctx interface{}, cfg interface{}) *Repository_UpdateReportConfigStatus_Call { - return &Repository_UpdateReportConfigStatus_Call{Call: _e.mock.On("UpdateReportConfigStatus", ctx, cfg)} -} - -func (_c *Repository_UpdateReportConfigStatus_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportConfigStatus_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 reports.ReportConfig - if args[1] != nil { - arg1 = args[1].(reports.ReportConfig) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateReportConfigStatus_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportConfigStatus_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_UpdateReportConfigStatus_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportConfigStatus_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportDue provides a mock function for the type Repository -func (_mock *Repository) UpdateReportDue(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, id, due) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportDue") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, id, due) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) reports.ReportConfig); ok { - r0 = returnFunc(ctx, id, due) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time) error); ok { - r1 = returnFunc(ctx, id, due) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateReportDue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportDue' -type Repository_UpdateReportDue_Call struct { - *mock.Call -} - -// UpdateReportDue is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - due time.Time -func (_e *Repository_Expecter) UpdateReportDue(ctx interface{}, id interface{}, due interface{}) *Repository_UpdateReportDue_Call { - return &Repository_UpdateReportDue_Call{Call: _e.mock.On("UpdateReportDue", ctx, id, due)} -} - -func (_c *Repository_UpdateReportDue_Call) Run(run func(ctx context.Context, id string, due time.Time)) *Repository_UpdateReportDue_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 time.Time - if args[2] != nil { - arg2 = args[2].(time.Time) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_UpdateReportDue_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportDue_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_UpdateReportDue_Call) RunAndReturn(run func(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error)) *Repository_UpdateReportDue_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportSchedule provides a mock function for the type Repository -func (_mock *Repository) UpdateReportSchedule(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportSchedule") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_UpdateReportSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportSchedule' -type Repository_UpdateReportSchedule_Call struct { - *mock.Call -} - -// UpdateReportSchedule is a helper method to define mock.On call -// - ctx context.Context -// - cfg reports.ReportConfig -func (_e *Repository_Expecter) UpdateReportSchedule(ctx interface{}, cfg interface{}) *Repository_UpdateReportSchedule_Call { - return &Repository_UpdateReportSchedule_Call{Call: _e.mock.On("UpdateReportSchedule", ctx, cfg)} -} - -func (_c *Repository_UpdateReportSchedule_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 reports.ReportConfig - if args[1] != nil { - arg1 = args[1].(reports.ReportConfig) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_UpdateReportSchedule_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportSchedule_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_UpdateReportSchedule_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportSchedule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportTemplate provides a mock function for the type Repository -func (_mock *Repository) UpdateReportTemplate(ctx context.Context, domainID string, reportID string, template reports.ReportTemplate) error { - ret := _mock.Called(ctx, domainID, reportID, template) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportTemplate") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, reports.ReportTemplate) error); ok { - r0 = returnFunc(ctx, domainID, reportID, template) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Repository_UpdateReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportTemplate' -type Repository_UpdateReportTemplate_Call struct { - *mock.Call -} - -// UpdateReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - domainID string -// - reportID string -// - template reports.ReportTemplate -func (_e *Repository_Expecter) UpdateReportTemplate(ctx interface{}, domainID interface{}, reportID interface{}, template interface{}) *Repository_UpdateReportTemplate_Call { - return &Repository_UpdateReportTemplate_Call{Call: _e.mock.On("UpdateReportTemplate", ctx, domainID, reportID, template)} -} - -func (_c *Repository_UpdateReportTemplate_Call) Run(run func(ctx context.Context, domainID string, reportID string, template reports.ReportTemplate)) *Repository_UpdateReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - var arg3 reports.ReportTemplate - if args[3] != nil { - arg3 = args[3].(reports.ReportTemplate) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *Repository_UpdateReportTemplate_Call) Return(err error) *Repository_UpdateReportTemplate_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Repository_UpdateReportTemplate_Call) RunAndReturn(run func(ctx context.Context, domainID string, reportID string, template reports.ReportTemplate) error) *Repository_UpdateReportTemplate_Call { - _c.Call.Return(run) - return _c -} - -// ViewReportConfig provides a mock function for the type Repository -func (_mock *Repository) ViewReportConfig(ctx context.Context, id string) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for ViewReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) reports.ReportConfig); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = returnFunc(ctx, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ViewReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportConfig' -type Repository_ViewReportConfig_Call struct { - *mock.Call -} - -// ViewReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *Repository_Expecter) ViewReportConfig(ctx interface{}, id interface{}) *Repository_ViewReportConfig_Call { - return &Repository_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, id)} -} - -func (_c *Repository_ViewReportConfig_Call) Run(run func(ctx context.Context, id string)) *Repository_ViewReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *Repository_ViewReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_ViewReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Repository_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Context, id string) (reports.ReportConfig, error)) *Repository_ViewReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// ViewReportTemplate provides a mock function for the type Repository -func (_mock *Repository) ViewReportTemplate(ctx context.Context, domainID string, reportID string) (reports.ReportTemplate, error) { - ret := _mock.Called(ctx, domainID, reportID) - - if len(ret) == 0 { - panic("no return value specified for ViewReportTemplate") - } - - var r0 reports.ReportTemplate - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (reports.ReportTemplate, error)); ok { - return returnFunc(ctx, domainID, reportID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) reports.ReportTemplate); ok { - r0 = returnFunc(ctx, domainID, reportID) - } else { - r0 = ret.Get(0).(reports.ReportTemplate) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = returnFunc(ctx, domainID, reportID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Repository_ViewReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportTemplate' -type Repository_ViewReportTemplate_Call struct { - *mock.Call -} - -// ViewReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - domainID string -// - reportID string -func (_e *Repository_Expecter) ViewReportTemplate(ctx interface{}, domainID interface{}, reportID interface{}) *Repository_ViewReportTemplate_Call { - return &Repository_ViewReportTemplate_Call{Call: _e.mock.On("ViewReportTemplate", ctx, domainID, reportID)} -} - -func (_c *Repository_ViewReportTemplate_Call) Run(run func(ctx context.Context, domainID string, reportID string)) *Repository_ViewReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Repository_ViewReportTemplate_Call) Return(reportTemplate reports.ReportTemplate, err error) *Repository_ViewReportTemplate_Call { - _c.Call.Return(reportTemplate, err) - return _c -} - -func (_c *Repository_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, domainID string, reportID string) (reports.ReportTemplate, error)) *Repository_ViewReportTemplate_Call { - _c.Call.Return(run) - return _c -} diff --git a/reports/mocks/service.go b/reports/mocks/service.go deleted file mode 100644 index 96fe7bdd1..000000000 --- a/reports/mocks/service.go +++ /dev/null @@ -1,944 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/reports" - mock "github.com/stretchr/testify/mock" -) - -// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewService(t interface { - mock.TestingT - Cleanup(func()) -}) *Service { - mock := &Service{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// Service is an autogenerated mock type for the Service type -type Service struct { - mock.Mock -} - -type Service_Expecter struct { - mock *mock.Mock -} - -func (_m *Service) EXPECT() *Service_Expecter { - return &Service_Expecter{mock: &_m.Mock} -} - -// AddReportConfig provides a mock function for the type Service -func (_mock *Service) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for AddReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, session, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_AddReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddReportConfig' -type Service_AddReportConfig_Call struct { - *mock.Call -} - -// AddReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - cfg reports.ReportConfig -func (_e *Service_Expecter) AddReportConfig(ctx interface{}, session interface{}, cfg interface{}) *Service_AddReportConfig_Call { - return &Service_AddReportConfig_Call{Call: _e.mock.On("AddReportConfig", ctx, session, cfg)} -} - -func (_c *Service_AddReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_AddReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.ReportConfig - if args[2] != nil { - arg2 = args[2].(reports.ReportConfig) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_AddReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_AddReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_AddReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_AddReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// DeleteReportTemplate provides a mock function for the type Service -func (_mock *Service) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteReportTemplate") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_DeleteReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteReportTemplate' -type Service_DeleteReportTemplate_Call struct { - *mock.Call -} - -// DeleteReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) DeleteReportTemplate(ctx interface{}, session interface{}, id interface{}) *Service_DeleteReportTemplate_Call { - return &Service_DeleteReportTemplate_Call{Call: _e.mock.On("DeleteReportTemplate", ctx, session, id)} -} - -func (_c *Service_DeleteReportTemplate_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DeleteReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_DeleteReportTemplate_Call) Return(err error) *Service_DeleteReportTemplate_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_DeleteReportTemplate_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_DeleteReportTemplate_Call { - _c.Call.Return(run) - return _c -} - -// DisableReportConfig provides a mock function for the type Service -func (_mock *Service) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DisableReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_DisableReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableReportConfig' -type Service_DisableReportConfig_Call struct { - *mock.Call -} - -// DisableReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) DisableReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_DisableReportConfig_Call { - return &Service_DisableReportConfig_Call{Call: _e.mock.On("DisableReportConfig", ctx, session, id)} -} - -func (_c *Service_DisableReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DisableReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_DisableReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_DisableReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_DisableReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error)) *Service_DisableReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// EnableReportConfig provides a mock function for the type Service -func (_mock *Service) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for EnableReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_EnableReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableReportConfig' -type Service_EnableReportConfig_Call struct { - *mock.Call -} - -// EnableReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) EnableReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_EnableReportConfig_Call { - return &Service_EnableReportConfig_Call{Call: _e.mock.On("EnableReportConfig", ctx, session, id)} -} - -func (_c *Service_EnableReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_EnableReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_EnableReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_EnableReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_EnableReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error)) *Service_EnableReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// GenerateReport provides a mock function for the type Service -func (_mock *Service) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { - ret := _mock.Called(ctx, session, config, action) - - if len(ret) == 0 { - panic("no return value specified for GenerateReport") - } - - var r0 reports.ReportPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) (reports.ReportPage, error)); ok { - return returnFunc(ctx, session, config, action) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) reports.ReportPage); ok { - r0 = returnFunc(ctx, session, config, action) - } else { - r0 = ret.Get(0).(reports.ReportPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) error); ok { - r1 = returnFunc(ctx, session, config, action) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_GenerateReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateReport' -type Service_GenerateReport_Call struct { - *mock.Call -} - -// GenerateReport is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - config reports.ReportConfig -// - action reports.ReportAction -func (_e *Service_Expecter) GenerateReport(ctx interface{}, session interface{}, config interface{}, action interface{}) *Service_GenerateReport_Call { - return &Service_GenerateReport_Call{Call: _e.mock.On("GenerateReport", ctx, session, config, action)} -} - -func (_c *Service_GenerateReport_Call) Run(run func(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction)) *Service_GenerateReport_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.ReportConfig - if args[2] != nil { - arg2 = args[2].(reports.ReportConfig) - } - var arg3 reports.ReportAction - if args[3] != nil { - arg3 = args[3].(reports.ReportAction) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *Service_GenerateReport_Call) Return(reportPage reports.ReportPage, err error) *Service_GenerateReport_Call { - _c.Call.Return(reportPage, err) - return _c -} - -func (_c *Service_GenerateReport_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error)) *Service_GenerateReport_Call { - _c.Call.Return(run) - return _c -} - -// ListReportsConfig provides a mock function for the type Service -func (_mock *Service) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { - ret := _mock.Called(ctx, session, pm) - - if len(ret) == 0 { - panic("no return value specified for ListReportsConfig") - } - - var r0 reports.ReportConfigPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.PageMeta) (reports.ReportConfigPage, error)); ok { - return returnFunc(ctx, session, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.PageMeta) reports.ReportConfigPage); ok { - r0 = returnFunc(ctx, session, pm) - } else { - r0 = ret.Get(0).(reports.ReportConfigPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.PageMeta) error); ok { - r1 = returnFunc(ctx, session, pm) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ListReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReportsConfig' -type Service_ListReportsConfig_Call struct { - *mock.Call -} - -// ListReportsConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - pm reports.PageMeta -func (_e *Service_Expecter) ListReportsConfig(ctx interface{}, session interface{}, pm interface{}) *Service_ListReportsConfig_Call { - return &Service_ListReportsConfig_Call{Call: _e.mock.On("ListReportsConfig", ctx, session, pm)} -} - -func (_c *Service_ListReportsConfig_Call) Run(run func(ctx context.Context, session authn.Session, pm reports.PageMeta)) *Service_ListReportsConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.PageMeta - if args[2] != nil { - arg2 = args[2].(reports.PageMeta) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_ListReportsConfig_Call) Return(reportConfigPage reports.ReportConfigPage, err error) *Service_ListReportsConfig_Call { - _c.Call.Return(reportConfigPage, err) - return _c -} - -func (_c *Service_ListReportsConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error)) *Service_ListReportsConfig_Call { - _c.Call.Return(run) - return _c -} - -// RemoveReportConfig provides a mock function for the type Service -func (_mock *Service) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for RemoveReportConfig") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_RemoveReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReportConfig' -type Service_RemoveReportConfig_Call struct { - *mock.Call -} - -// RemoveReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) RemoveReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_RemoveReportConfig_Call { - return &Service_RemoveReportConfig_Call{Call: _e.mock.On("RemoveReportConfig", ctx, session, id)} -} - -func (_c *Service_RemoveReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_RemoveReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_RemoveReportConfig_Call) Return(err error) *Service_RemoveReportConfig_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_RemoveReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_RemoveReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// StartScheduler provides a mock function for the type Service -func (_mock *Service) StartScheduler(ctx context.Context) error { - ret := _mock.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for StartScheduler") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = returnFunc(ctx) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_StartScheduler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartScheduler' -type Service_StartScheduler_Call struct { - *mock.Call -} - -// StartScheduler is a helper method to define mock.On call -// - ctx context.Context -func (_e *Service_Expecter) StartScheduler(ctx interface{}) *Service_StartScheduler_Call { - return &Service_StartScheduler_Call{Call: _e.mock.On("StartScheduler", ctx)} -} - -func (_c *Service_StartScheduler_Call) Run(run func(ctx context.Context)) *Service_StartScheduler_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *Service_StartScheduler_Call) Return(err error) *Service_StartScheduler_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_StartScheduler_Call) RunAndReturn(run func(ctx context.Context) error) *Service_StartScheduler_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportConfig provides a mock function for the type Service -func (_mock *Service) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, session, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfig' -type Service_UpdateReportConfig_Call struct { - *mock.Call -} - -// UpdateReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - cfg reports.ReportConfig -func (_e *Service_Expecter) UpdateReportConfig(ctx interface{}, session interface{}, cfg interface{}) *Service_UpdateReportConfig_Call { - return &Service_UpdateReportConfig_Call{Call: _e.mock.On("UpdateReportConfig", ctx, session, cfg)} -} - -func (_c *Service_UpdateReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_UpdateReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.ReportConfig - if args[2] != nil { - arg2 = args[2].(reports.ReportConfig) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_UpdateReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_UpdateReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_UpdateReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportSchedule provides a mock function for the type Service -func (_mock *Service) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportSchedule") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok { - r1 = returnFunc(ctx, session, cfg) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_UpdateReportSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportSchedule' -type Service_UpdateReportSchedule_Call struct { - *mock.Call -} - -// UpdateReportSchedule is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - cfg reports.ReportConfig -func (_e *Service_Expecter) UpdateReportSchedule(ctx interface{}, session interface{}, cfg interface{}) *Service_UpdateReportSchedule_Call { - return &Service_UpdateReportSchedule_Call{Call: _e.mock.On("UpdateReportSchedule", ctx, session, cfg)} -} - -func (_c *Service_UpdateReportSchedule_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_UpdateReportSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.ReportConfig - if args[2] != nil { - arg2 = args[2].(reports.ReportConfig) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateReportSchedule_Call) Return(reportConfig reports.ReportConfig, err error) *Service_UpdateReportSchedule_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_UpdateReportSchedule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_UpdateReportSchedule_Call { - _c.Call.Return(run) - return _c -} - -// UpdateReportTemplate provides a mock function for the type Service -func (_mock *Service) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportTemplate") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) error); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// Service_UpdateReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportTemplate' -type Service_UpdateReportTemplate_Call struct { - *mock.Call -} - -// UpdateReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - cfg reports.ReportConfig -func (_e *Service_Expecter) UpdateReportTemplate(ctx interface{}, session interface{}, cfg interface{}) *Service_UpdateReportTemplate_Call { - return &Service_UpdateReportTemplate_Call{Call: _e.mock.On("UpdateReportTemplate", ctx, session, cfg)} -} - -func (_c *Service_UpdateReportTemplate_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_UpdateReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 reports.ReportConfig - if args[2] != nil { - arg2 = args[2].(reports.ReportConfig) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_UpdateReportTemplate_Call) Return(err error) *Service_UpdateReportTemplate_Call { - _c.Call.Return(err) - return _c -} - -func (_c *Service_UpdateReportTemplate_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error) *Service_UpdateReportTemplate_Call { - _c.Call.Return(run) - return _c -} - -// ViewReportConfig provides a mock function for the type Service -func (_mock *Service) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { - ret := _mock.Called(ctx, session, id, withRoles) - - if len(ret) == 0 { - panic("no return value specified for ViewReportConfig") - } - - var r0 reports.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) (reports.ReportConfig, error)); ok { - return returnFunc(ctx, session, id, withRoles) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) reports.ReportConfig); ok { - r0 = returnFunc(ctx, session, id, withRoles) - } else { - r0 = ret.Get(0).(reports.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string, bool) error); ok { - r1 = returnFunc(ctx, session, id, withRoles) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ViewReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportConfig' -type Service_ViewReportConfig_Call struct { - *mock.Call -} - -// ViewReportConfig is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -// - withRoles bool -func (_e *Service_Expecter) ViewReportConfig(ctx interface{}, session interface{}, id interface{}, withRoles interface{}) *Service_ViewReportConfig_Call { - return &Service_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, session, id, withRoles)} -} - -func (_c *Service_ViewReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string, withRoles bool)) *Service_ViewReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - var arg3 bool - if args[3] != nil { - arg3 = args[3].(bool) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *Service_ViewReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_ViewReportConfig_Call { - _c.Call.Return(reportConfig, err) - return _c -} - -func (_c *Service_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error)) *Service_ViewReportConfig_Call { - _c.Call.Return(run) - return _c -} - -// ViewReportTemplate provides a mock function for the type Service -func (_mock *Service) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewReportTemplate") - } - - var r0 reports.ReportTemplate - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportTemplate, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportTemplate); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(reports.ReportTemplate) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = returnFunc(ctx, session, id) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// Service_ViewReportTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportTemplate' -type Service_ViewReportTemplate_Call struct { - *mock.Call -} - -// ViewReportTemplate is a helper method to define mock.On call -// - ctx context.Context -// - session authn.Session -// - id string -func (_e *Service_Expecter) ViewReportTemplate(ctx interface{}, session interface{}, id interface{}) *Service_ViewReportTemplate_Call { - return &Service_ViewReportTemplate_Call{Call: _e.mock.On("ViewReportTemplate", ctx, session, id)} -} - -func (_c *Service_ViewReportTemplate_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewReportTemplate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 authn.Session - if args[1] != nil { - arg1 = args[1].(authn.Session) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *Service_ViewReportTemplate_Call) Return(reportTemplate reports.ReportTemplate, err error) *Service_ViewReportTemplate_Call { - _c.Call.Return(reportTemplate, err) - return _c -} - -func (_c *Service_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error)) *Service_ViewReportTemplate_Call { - _c.Call.Return(run) - return _c -} diff --git a/reports/operations/operations.go b/reports/operations/operations.go deleted file mode 100644 index 92f85b47c..000000000 --- a/reports/operations/operations.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package operations - -import "github.com/absmach/magistrala/pkg/permissions" - -const EntityType = "report" - -// Report Operations. -const ( - OpAddReportConfig permissions.Operation = iota - OpViewReportConfig - OpUpdateReportConfig - OpUpdateReportSchedule - OpRemoveReportConfig - OpListReportsConfig - OpEnableReportConfig - OpDisableReportConfig - OpGenerateReport - OpUpdateReportTemplate - OpViewReportTemplate - OpDeleteReportTemplate -) - -func OperationDetails() map[permissions.Operation]permissions.OperationDetails { - return map[permissions.Operation]permissions.OperationDetails{ - OpAddReportConfig: { - Name: "create", - PermissionRequired: true, - }, - OpViewReportConfig: { - Name: "view", - PermissionRequired: true, - }, - OpUpdateReportConfig: { - Name: "update", - PermissionRequired: true, - }, - OpUpdateReportSchedule: { - Name: "update_schedule", - PermissionRequired: true, - }, - OpRemoveReportConfig: { - Name: "delete", - PermissionRequired: true, - }, - OpListReportsConfig: { - Name: "list", - PermissionRequired: true, - }, - OpEnableReportConfig: { - Name: "enable", - PermissionRequired: true, - }, - OpDisableReportConfig: { - Name: "disable", - PermissionRequired: true, - }, - OpGenerateReport: { - Name: "generate", - PermissionRequired: true, - }, - OpUpdateReportTemplate: { - Name: "update_template", - PermissionRequired: true, - }, - OpViewReportTemplate: { - Name: "view_template", - PermissionRequired: true, - }, - OpDeleteReportTemplate: { - Name: "delete_template", - PermissionRequired: true, - }, - } -} diff --git a/reports/postgres/errors.go b/reports/postgres/errors.go deleted file mode 100644 index eb3babb61..000000000 --- a/reports/postgres/errors.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" -) - -var _ errors.Mapper = (*duplicateErrors)(nil) - -type duplicateErrors struct{} - -// GetError maps constraint names to known errors. -func (d duplicateErrors) GetError(constraint string) (error, bool) { - switch constraint { - case "report_config_pkey": - return repoerr.ErrConflict, true - default: - return nil, false - } -} - -func NewDuplicateErrors() errors.Mapper { - return duplicateErrors{} -} diff --git a/reports/postgres/init.go b/reports/postgres/init.go deleted file mode 100644 index 03fdb8bec..000000000 --- a/reports/postgres/init.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - migrate "github.com/rubenv/sql-migrate" -) - -func Migration() (*migrate.MemoryMigrationSource, error) { - reportsMigration := &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "reports_01", - Up: []string{ - `CREATE TABLE IF NOT EXISTS report_config ( - id VARCHAR(36) PRIMARY KEY, - name VARCHAR(1024), - description TEXT, - domain_id VARCHAR(36) NOT NULL, - status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), - created_at TIMESTAMP, - created_by VARCHAR(254), - updated_at TIMESTAMP, - updated_by VARCHAR(254), - due TIMESTAMPTZ, - recurring SMALLINT, - recurring_period SMALLINT, - start_datetime TIMESTAMP, - config JSONB, - email JSONB, - metrics JSONB - );`, - }, - Down: []string{ - `DROP TABLE IF EXISTS report_config;`, - }, - }, - { - Id: "reports_02", - Up: []string{ - `ALTER TABLE report_config ADD COLUMN report_template TEXT;`, - }, - Down: []string{ - `ALTER TABLE report_config DROP COLUMN report_template;`, - }, - }, - { - Id: "reports_03", - Up: []string{ - // Canonicalize legacy report metric subtopics from dot/NATS wildcards - // to slash/MQTT wildcards. - `UPDATE report_config AS rc - SET metrics = COALESCE(( - SELECT jsonb_agg( - CASE - WHEN metric.elem ? 'subtopic' - AND jsonb_typeof(metric.elem->'subtopic') = 'string' - THEN jsonb_set( - metric.elem, - '{subtopic}', - to_jsonb(REPLACE(REPLACE(REPLACE(metric.elem->>'subtopic', '>', '#'), '*', '+'), '.', '/')), - false - ) - ELSE metric.elem - END - ORDER BY metric.ord - ) - FROM jsonb_array_elements(rc.metrics) WITH ORDINALITY AS metric(elem, ord) - ), '[]'::jsonb) - WHERE jsonb_typeof(rc.metrics) = 'array'`, - }, - Down: []string{ - `UPDATE report_config AS rc - SET metrics = COALESCE(( - SELECT jsonb_agg( - CASE - WHEN metric.elem ? 'subtopic' - AND jsonb_typeof(metric.elem->'subtopic') = 'string' - THEN jsonb_set( - metric.elem, - '{subtopic}', - to_jsonb(REPLACE(REPLACE(REPLACE(metric.elem->>'subtopic', '#', '>'), '+', '*'), '/', '.')), - false - ) - ELSE metric.elem - END - ORDER BY metric.ord - ) - FROM jsonb_array_elements(rc.metrics) WITH ORDINALITY AS metric(elem, ord) - ), '[]'::jsonb) - WHERE jsonb_typeof(rc.metrics) = 'array'`, - }, - }, - }, - } - - return reportsMigration, nil -} diff --git a/reports/postgres/reports.go b/reports/postgres/reports.go deleted file mode 100644 index 86a2da1f7..000000000 --- a/reports/postgres/reports.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "database/sql" - "encoding/json" - "time" - - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/reports" -) - -// dbReport represents the database structure for a Report. -type dbReport struct { - ID string `db:"id"` - Name string `db:"name"` - Description string `db:"description"` - DomainID string `db:"domain_id"` - StartDateTime sql.NullTime `db:"start_datetime"` - Due sql.NullTime `db:"due"` - Recurring schedule.Recurring `db:"recurring"` - RecurringPeriod uint `db:"recurring_period"` - Status reports.Status `db:"status"` - CreatedAt time.Time `db:"created_at"` - CreatedBy string `db:"created_by"` - UpdatedAt time.Time `db:"updated_at"` - UpdatedBy string `db:"updated_by"` - Config []byte `db:"config,omitempty"` - Metrics []byte `db:"metrics"` - Email []byte `db:"email"` - ReportTemplate reports.ReportTemplate `db:"report_template"` -} - -func reportToDb(r reports.ReportConfig) (dbReport, error) { - config := []byte("{}") - if r.Config != nil { - b, err := json.Marshal(r.Config) - if err != nil { - return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - config = b - } - - metrics := []byte("{}") - if r.Metrics != nil { - m, err := json.Marshal(r.Metrics) - if err != nil { - return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - metrics = m - } - - email := []byte("{}") - if r.Email != nil { - e, err := json.Marshal(r.Email) - if err != nil { - return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - email = e - } - start := sql.NullTime{Time: r.Schedule.StartDateTime} - if !r.Schedule.StartDateTime.IsZero() { - start.Valid = true - } - t := sql.NullTime{Time: r.Schedule.Time} - if !r.Schedule.Time.IsZero() { - t.Valid = true - } - - return dbReport{ - ID: r.ID, - Name: r.Name, - Description: r.Description, - DomainID: r.DomainID, - StartDateTime: start, - Due: t, - Recurring: r.Schedule.Recurring, - RecurringPeriod: r.Schedule.RecurringPeriod, - Status: r.Status, - CreatedAt: r.CreatedAt, - CreatedBy: r.CreatedBy, - UpdatedAt: r.UpdatedAt, - UpdatedBy: r.UpdatedBy, - Config: config, - Metrics: metrics, - Email: email, - ReportTemplate: r.ReportTemplate, - }, nil -} - -func dbToReport(dto dbReport) (reports.ReportConfig, error) { - var config reports.MetricConfig - if dto.Config != nil { - if err := json.Unmarshal(dto.Config, &config); err != nil { - return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - var email reports.EmailSetting - if dto.Email != nil { - if err := json.Unmarshal(dto.Email, &email); err != nil { - return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - var metrics []reports.ReqMetric - if dto.Metrics != nil { - if err := json.Unmarshal(dto.Metrics, &metrics); err != nil { - return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - rpt := reports.ReportConfig{ - ID: dto.ID, - Name: dto.Name, - Description: dto.Description, - DomainID: dto.DomainID, - Config: &config, - Metrics: metrics, - Schedule: schedule.Schedule{ - StartDateTime: dto.StartDateTime.Time, - Time: dto.Due.Time, - Recurring: dto.Recurring, - RecurringPeriod: dto.RecurringPeriod, - }, - Email: &email, - Status: dto.Status, - CreatedAt: dto.CreatedAt, - CreatedBy: dto.CreatedBy, - UpdatedAt: dto.UpdatedAt, - UpdatedBy: dto.UpdatedBy, - ReportTemplate: dto.ReportTemplate, - } - - return rpt, nil -} diff --git a/reports/postgres/repository.go b/reports/postgres/repository.go deleted file mode 100644 index 4098ef457..000000000 --- a/reports/postgres/repository.go +++ /dev/null @@ -1,451 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "database/sql" - "fmt" - "strings" - "time" - - api "github.com/absmach/magistrala/api/http" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/postgres" - "github.com/absmach/magistrala/reports" -) - -type PostgresRepository struct { - DB postgres.Database - eh errors.Handler -} - -func NewRepository(db postgres.Database) reports.Repository { - errHandlerOptions := []errors.HandlerOption{ - postgres.WithDuplicateErrors(NewDuplicateErrors()), - } - return &PostgresRepository{ - DB: db, - eh: postgres.NewErrorHandler(errHandlerOptions...), - } -} - -func (repo *PostgresRepository) AddReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - q := ` - INSERT INTO report_config (id, name, description, domain_id, config, metrics, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status, report_template) - VALUES (:id, :name, :description, :domain_id, :config, :metrics, - :email, :start_datetime, :due, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status, :report_template) - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status, report_template; - ` - dbr, err := reportToDb(cfg) - if err != nil { - return reports.ReportConfig{}, repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return reports.ReportConfig{}, repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - defer row.Close() - - var dbReport dbReport - if row.Next() { - if err := row.StructScan(&dbReport); err != nil { - return reports.ReportConfig{}, repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - } - - report, err := dbToReport(dbReport) - if err != nil { - return reports.ReportConfig{}, repo.eh.HandleError(repoerr.ErrCreateEntity, err) - } - - return report, nil -} - -func (repo *PostgresRepository) ViewReportConfig(ctx context.Context, id string) (reports.ReportConfig, error) { - q := ` - SELECT id, name, description, domain_id, config, metrics, report_template, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status - FROM report_config - WHERE id = $1; - ` - row := repo.DB.QueryRowxContext(ctx, q, id) - if err := row.Err(); err != nil { - return reports.ReportConfig{}, err - } - var dbr dbReport - if err := row.StructScan(&dbr); err != nil { - if err == sql.ErrNoRows { - return reports.ReportConfig{}, repoerr.ErrNotFound - } - return reports.ReportConfig{}, err - } - rpt, err := dbToReport(dbr) - if err != nil { - return reports.ReportConfig{}, err - } - - return rpt, nil -} - -func (repo *PostgresRepository) UpdateReportConfigStatus(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - q := `UPDATE report_config SET status = :status, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id - RETURNING id, name, description, domain_id, metrics, email, config, - start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;` - - dbRpt, err := reportToDb(cfg) - if err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - row, err := repo.DB.NamedQueryContext(ctx, q, dbRpt) - if err != nil { - return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - dbr := dbReport{} - if row.Next() { - if err := row.StructScan(&dbr); err != nil { - return reports.ReportConfig{}, err - } - - res, err := dbToReport(dbr) - if err != nil { - return reports.ReportConfig{}, err - } - return res, err - } - - return reports.ReportConfig{}, repoerr.ErrNotFound -} - -func (repo *PostgresRepository) UpdateReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - var query []string - - if cfg.Name != "" { - query = append(query, "name = :name") - } - - if cfg.Description != "" { - query = append(query, "description = :description") - } - - if len(cfg.Metrics) > 0 { - query = append(query, "metrics = :metrics") - } - - if cfg.Email != nil { - query = append(query, "email = :email") - } - - if cfg.Config != nil { - query = append(query, "config = :config") - } - - var q string - if len(query) > 0 { - q = strings.Join(query, ", ") - } - - q = fmt.Sprintf(` - UPDATE report_config - SET %s, - updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - `, q) - - dbr, err := reportToDb(cfg) - if err != nil { - return reports.ReportConfig{}, err - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return reports.ReportConfig{}, err - } - defer row.Close() - - var dbReport dbReport - if !row.Next() { - if err := row.Err(); err != nil { - return reports.ReportConfig{}, err - } - return reports.ReportConfig{}, repoerr.ErrNotFound - } - if err := row.StructScan(&dbReport); err != nil { - return reports.ReportConfig{}, err - } - rpt, err := dbToReport(dbReport) - if err != nil { - return reports.ReportConfig{}, err - } - - return rpt, nil -} - -func (repo *PostgresRepository) UpdateReportSchedule(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) { - q := ` - UPDATE report_config - SET start_datetime = :start_datetime, due = :due, recurring = :recurring, - recurring_period = :recurring_period, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - - dbr, err := reportToDb(cfg) - if err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - var dbReport dbReport - if !row.Next() { - if err := row.Err(); err != nil { - return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - return reports.ReportConfig{}, repoerr.ErrNotFound - } - if err := row.StructScan(&dbReport); err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - report, err := dbToReport(dbReport) - if err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - return report, nil -} - -func (repo *PostgresRepository) RemoveReportConfig(ctx context.Context, id string) error { - q := ` - DELETE FROM report_config - WHERE id = $1; - ` - - result, err := repo.DB.ExecContext(ctx, q, id) - if err != nil { - return err - } - - if _, err := result.RowsAffected(); err != nil { - return repoerr.ErrNotFound - } - - return nil -} - -func (repo *PostgresRepository) ListAllReportsConfig(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error) { - listReportsQuery := ` - SELECT id, name, description, domain_id, metrics, email, config, - start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status - FROM report_config rc %s %s %s; - ` - - pq := pageReportQuery(pm) - orderClause := reportsOrderClause(pm) - pgData := reportsPageData(pm) - - q := fmt.Sprintf(listReportsQuery, pq, orderClause, pgData) - rows, err := repo.DB.NamedQueryContext(ctx, q, pm) - if err != nil { - return reports.ReportConfigPage{}, err - } - defer rows.Close() - - cfgs := []reports.ReportConfig{} - for rows.Next() { - var r dbReport - if err := rows.StructScan(&r); err != nil { - return reports.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - rpt, err := dbToReport(r) - if err != nil { - return reports.ReportConfigPage{}, err - } - cfgs = append(cfgs, rpt) - } - - cq := fmt.Sprintf(`SELECT COUNT(*) FROM report_config rc %s;`, pq) - - total, err := postgres.Total(ctx, repo.DB, cq, pm) - if err != nil { - return reports.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - pm.Total = total - ret := reports.ReportConfigPage{ - PageMeta: pm, - ReportConfigs: cfgs, - } - - return ret, nil -} - -func (repo *PostgresRepository) UpdateReportDue(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error) { - q := ` - UPDATE report_config - SET due = :due, updated_at = :updated_at WHERE id = :id - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - - dbr := dbReport{ - ID: id, - UpdatedAt: time.Now().UTC(), - Due: sql.NullTime{Time: due}, - } - if !due.IsZero() { - dbr.Due.Valid = true - } - - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - var dbReport dbReport - if !row.Next() { - if err := row.Err(); err != nil { - return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - return reports.ReportConfig{}, repoerr.ErrNotFound - } - if err := row.StructScan(&dbReport); err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - report, err := dbToReport(dbReport) - if err != nil { - return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - return report, nil -} - -func (repo *PostgresRepository) UpdateReportTemplate(ctx context.Context, domainID, reportID string, template reports.ReportTemplate) error { - q := ` - UPDATE report_config - SET report_template = :report_template, updated_at = :updated_at - WHERE id = :id AND domain_id = :domain_id` - - dbr := dbReport{ - ID: reportID, - DomainID: domainID, - UpdatedAt: time.Now().UTC(), - ReportTemplate: template, - } - - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return errors.Wrap(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - return nil -} - -func (repo *PostgresRepository) ViewReportTemplate(ctx context.Context, domainID, reportID string) (reports.ReportTemplate, error) { - q := ` - SELECT COALESCE(report_template, '') as report_template - FROM report_config - WHERE id = $1 AND domain_id = $2` - - var template reports.ReportTemplate - err := repo.DB.QueryRowxContext(ctx, q, reportID, domainID).Scan(&template) - if err != nil { - if err == sql.ErrNoRows { - return "", repoerr.ErrNotFound - } - return "", errors.Wrap(repoerr.ErrViewEntity, err) - } - - return template, nil -} - -func (repo *PostgresRepository) DeleteReportTemplate(ctx context.Context, domainID, reportID string) error { - q := ` - UPDATE report_config - SET report_template = '', updated_at = :updated_at - WHERE id = :id AND domain_id = :domain_id` - - dbr := dbReport{ - ID: reportID, - DomainID: domainID, - UpdatedAt: time.Now().UTC(), - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return errors.Wrap(repoerr.ErrRemoveEntity, err) - } - - defer row.Close() - - return nil -} - -func reportsOrderClause(pm reports.PageMeta) string { - dir := api.DescDir - if pm.Dir == api.AscDir { - dir = api.AscDir - } - switch pm.Order { - case api.NameKey: - return fmt.Sprintf("ORDER BY name %s, id %s", dir, dir) - case api.CreatedAtOrder: - return fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir) - default: - return fmt.Sprintf("ORDER BY COALESCE(updated_at, created_at) %s, id %s", dir, dir) - } -} - -func reportsPageData(pm reports.PageMeta) string { - pgData := "" - if pm.Limit != 0 { - pgData = "LIMIT :limit" - } - if pm.Offset != 0 { - pgData += " OFFSET :offset" - } - return pgData -} - -func pageReportQuery(pm reports.PageMeta) string { - query := pageReportQueryConditions(pm) - - var q string - if len(query) > 0 { - q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - - return q -} - -func pageReportQueryConditions(pm reports.PageMeta) []string { - var query []string - if pm.Status != reports.AllStatus { - query = append(query, "rc.status = :status") - } - if pm.Domain != "" { - query = append(query, "rc.domain_id = :domain_id") - } - if pm.ScheduledBefore != nil { - query = append(query, "rc.due < :scheduled_before") - } - if pm.ScheduledAfter != nil { - query = append(query, "rc.due > :scheduled_after") - } - if pm.Name != "" { - query = append(query, "rc.name ILIKE '%' || :name || '%'") - } - return query -} diff --git a/reports/postgres/repository_test.go b/reports/postgres/repository_test.go deleted file mode 100644 index 14ae8408e..000000000 --- a/reports/postgres/repository_test.go +++ /dev/null @@ -1,825 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/postgres" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - namegen = namegenerator.NewGenerator() - idProvider = uuid.New() -) - -func generateUUID(t *testing.T) string { - id, err := idProvider.ID() - require.Nil(t, err, fmt.Sprintf("generate uuid unexpected error: %s", err)) - return id -} - -func TestAddReportConfig(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - Description: namegen.Generate(), - DomainID: generateUUID(t), - Config: &reports.MetricConfig{ - From: "now-1h", - To: "now", - Title: "Test Report", - }, - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - Email: &reports.EmailSetting{ - To: []string{"test@example.com"}, - Subject: "Test Report", - Content: "Report content", - }, - Schedule: schedule.Schedule{ - StartDateTime: time.Now().UTC(), - Time: time.Now().UTC().Add(time.Hour), - Recurring: schedule.Daily, - RecurringPeriod: 1, - }, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - } - - cases := []struct { - desc string - report reports.ReportConfig - err error - }{ - { - desc: "add valid report config", - report: reportConfig, - err: nil, - }, - { - desc: "add duplicate report config", - report: reportConfig, - err: repoerr.ErrConflict, - }, - { - desc: "add report config with empty ID", - report: reports.ReportConfig{ - Name: namegen.Generate(), - DomainID: generateUUID(t), - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - }, - err: repoerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.AddReportConfig(context.Background(), tc.report) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.NotEmpty(t, rpt.ID) - require.Equal(t, tc.report.Name, rpt.Name) - require.Equal(t, tc.report.DomainID, rpt.DomainID) - require.Equal(t, tc.report.Status, rpt.Status) - }) - } -} - -func TestViewReportConfig(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - Description: namegen.Generate(), - DomainID: generateUUID(t), - Config: &reports.MetricConfig{ - From: "now-1h", - To: "now", - Title: "Test Report", - }, - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - Email: &reports.EmailSetting{ - To: []string{"test@example.com"}, - Subject: "Test Report", - }, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "view existing report config", - id: saved.ID, - err: nil, - }, - { - desc: "view non-existing report config", - id: generateUUID(t), - err: repoerr.ErrNotFound, - }, - { - desc: "view with empty id", - id: "", - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.ViewReportConfig(context.Background(), tc.id) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, saved.ID, rpt.ID) - require.Equal(t, saved.Name, rpt.Name) - require.Equal(t, saved.DomainID, rpt.DomainID) - }) - } -} - -func TestUpdateReportConfig(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - Description: namegen.Generate(), - DomainID: generateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - report reports.ReportConfig - err error - }{ - { - desc: "update report name", - report: reports.ReportConfig{ - ID: saved.ID, - Name: "Updated Name", - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - }, - err: nil, - }, - { - desc: "update report description", - report: reports.ReportConfig{ - ID: saved.ID, - Description: "Updated Description", - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - }, - err: nil, - }, - { - desc: "update non-existing report", - report: reports.ReportConfig{ - ID: generateUUID(t), - Name: "New Name", - UpdatedAt: time.Now().UTC(), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.UpdateReportConfig(context.Background(), tc.report) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, tc.report.ID, rpt.ID) - if tc.report.Name != "" { - require.Equal(t, tc.report.Name, rpt.Name) - } - if tc.report.Description != "" { - require.Equal(t, tc.report.Description, rpt.Description) - } - }) - } -} - -func TestUpdateReportConfigStatus(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - Metrics: []reports.ReqMetric{}, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - report reports.ReportConfig - err error - }{ - { - desc: "disable report", - report: reports.ReportConfig{ - ID: saved.ID, - Status: reports.DisabledStatus, - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - }, - err: nil, - }, - { - desc: "enable report", - report: reports.ReportConfig{ - ID: saved.ID, - Status: reports.EnabledStatus, - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - }, - err: nil, - }, - { - desc: "update status of non-existing report", - report: reports.ReportConfig{ - ID: generateUUID(t), - Status: reports.DisabledStatus, - UpdatedAt: time.Now().UTC(), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.UpdateReportConfigStatus(context.Background(), tc.report) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, tc.report.Status, rpt.Status) - }) - } -} - -func TestRemoveReportConfig(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - Metrics: []reports.ReqMetric{}, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "remove existing report", - id: saved.ID, - err: nil, - }, - { - desc: "remove non-existing report", - id: generateUUID(t), - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.RemoveReportConfig(context.Background(), tc.id) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - }) - } -} - -func TestListReportsConfig(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - domainID := generateUUID(t) - - num := uint64(10) - for i := uint64(0); i < num; i++ { - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: fmt.Sprintf("Report-%d", i), - DomainID: domainID, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - Metrics: []reports.ReqMetric{}, - } - _, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - } - - cases := []struct { - desc string - pageMeta reports.PageMeta - size uint64 - err error - }{ - { - desc: "list all reports", - pageMeta: reports.PageMeta{ - Domain: domainID, - Limit: num, - Offset: 0, - }, - size: num, - err: nil, - }, - { - desc: "list with limit", - pageMeta: reports.PageMeta{ - Domain: domainID, - Limit: 5, - Offset: 0, - }, - size: 5, - err: nil, - }, - { - desc: "list with offset", - pageMeta: reports.PageMeta{ - Domain: domainID, - Limit: num, - Offset: 5, - }, - size: 5, - err: nil, - }, - { - desc: "list enabled reports", - pageMeta: reports.PageMeta{ - Domain: domainID, - Limit: num, - Status: reports.EnabledStatus, - }, - size: num, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - page, err := repo.ListAllReportsConfig(context.Background(), tc.pageMeta) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, tc.size, uint64(len(page.ReportConfigs))) - }) - } -} - -func TestUpdateReportSchedule(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - CreatedBy: generateUUID(t), - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - Schedule: schedule.Schedule{ - StartDateTime: time.Now().UTC(), - Time: time.Now().UTC().Add(time.Hour), - Recurring: schedule.Daily, - RecurringPeriod: 1, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - newSchedule := schedule.Schedule{ - StartDateTime: time.Now().UTC().Add(24 * time.Hour), - Time: time.Now().UTC().Add(25 * time.Hour), - Recurring: schedule.Weekly, - RecurringPeriod: 2, - } - - cases := []struct { - desc string - report reports.ReportConfig - expected schedule.Schedule - err error - }{ - { - desc: "update schedule", - report: reports.ReportConfig{ - ID: saved.ID, - Schedule: newSchedule, - UpdatedAt: time.Now().UTC(), - UpdatedBy: generateUUID(t), - }, - expected: newSchedule, - err: nil, - }, - { - desc: "update schedule of non-existing report", - report: reports.ReportConfig{ - ID: generateUUID(t), - Schedule: newSchedule, - UpdatedAt: time.Now().UTC(), - }, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.UpdateReportSchedule(context.Background(), tc.report) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, tc.expected.Recurring, rpt.Schedule.Recurring) - require.Equal(t, tc.expected.RecurringPeriod, rpt.Schedule.RecurringPeriod) - }) - } -} - -func TestUpdateReportDue(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: generateUUID(t), - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - newDue := time.Now().UTC().Add(24 * time.Hour).Truncate(time.Microsecond) - - cases := []struct { - desc string - id string - due time.Time - err error - }{ - { - desc: "update due time", - id: saved.ID, - due: newDue, - err: nil, - }, - { - desc: "update due time of non-existing report", - id: generateUUID(t), - due: newDue, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - rpt, err := repo.UpdateReportDue(context.Background(), tc.id, tc.due) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.True(t, tc.due.Equal(rpt.Schedule.Time)) - }) - } -} - -func TestUpdateReportTemplate(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - domainID := generateUUID(t) - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - template := reports.ReportTemplate("Test Template") - - cases := []struct { - desc string - domainID string - reportID string - template reports.ReportTemplate - err error - }{ - { - desc: "update template", - domainID: domainID, - reportID: saved.ID, - template: template, - err: nil, - }, - { - desc: "update template for non-existing report", - domainID: domainID, - reportID: generateUUID(t), - template: template, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.UpdateReportTemplate(context.Background(), tc.domainID, tc.reportID, tc.template) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - }) - } -} - -func TestViewReportTemplate(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - domainID := generateUUID(t) - template := reports.ReportTemplate("Test Template") - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - ReportTemplate: template, - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - domainID string - reportID string - expected reports.ReportTemplate - err error - }{ - { - desc: "view existing template", - domainID: domainID, - reportID: saved.ID, - expected: template, - err: nil, - }, - { - desc: "view template for non-existing report", - domainID: domainID, - reportID: generateUUID(t), - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - tmpl, err := repo.ViewReportTemplate(context.Background(), tc.domainID, tc.reportID) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - require.Equal(t, tc.expected, tmpl) - }) - } -} - -func TestDeleteReportTemplate(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM report_config") - require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err)) - }) - - repo := postgres.NewRepository(database) - - domainID := generateUUID(t) - template := reports.ReportTemplate("Test Template") - - reportConfig := reports.ReportConfig{ - ID: generateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: reports.EnabledStatus, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - ReportTemplate: template, - Metrics: []reports.ReqMetric{ - { - ChannelID: generateUUID(t), - Name: "temperature", - }, - }, - } - - saved, err := repo.AddReportConfig(context.Background(), reportConfig) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := []struct { - desc string - domainID string - reportID string - err error - }{ - { - desc: "delete existing template", - domainID: domainID, - reportID: saved.ID, - err: nil, - }, - { - desc: "delete template for non-existing report", - domainID: domainID, - reportID: generateUUID(t), - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := repo.DeleteReportTemplate(context.Background(), tc.domainID, tc.reportID) - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - return - } - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - if tc.reportID == saved.ID { - tmpl, err := repo.ViewReportTemplate(context.Background(), tc.domainID, tc.reportID) - require.Nil(t, err) - require.Empty(t, tmpl) - } - }) - } -} diff --git a/reports/postgres/setup_test.go b/reports/postgres/setup_test.go deleted file mode 100644 index 2539bc5a6..000000000 --- a/reports/postgres/setup_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "database/sql" - "fmt" - "log" - "os" - "testing" - "time" - - "github.com/absmach/magistrala/pkg/postgres" - rpostgres "github.com/absmach/magistrala/reports/postgres" - "github.com/jmoiron/sqlx" - dockertest "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "go.opentelemetry.io/otel" -) - -var ( - db *sqlx.DB - database postgres.Database - tracer = otel.Tracer("repo_tests") -) - -func TestMain(m *testing.M) { - pool, err := dockertest.NewPool("") - if err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - container, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "postgres", - Tag: "16.2-alpine", - Env: []string{ - "POSTGRES_USER=test", - "POSTGRES_PASSWORD=test", - "POSTGRES_DB=test", - "listen_addresses = '*'", - }, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - if err != nil { - log.Fatalf("Could not start container: %s", err) - } - - port := container.GetPort("5432/tcp") - - pool.MaxWait = 120 * time.Second - if err := pool.Retry(func() error { - url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port) - db, err := sql.Open("pgx", url) - if err != nil { - return err - } - return db.Ping() - }); err != nil { - log.Fatalf("Could not connect to docker: %s", err) - } - - dbConfig := postgres.Config{ - Host: "localhost", - Port: port, - User: "test", - Pass: "test", - Name: "test", - SSLMode: "disable", - SSLCert: "", - SSLKey: "", - SSLRootCert: "", - } - - migration, err := rpostgres.Migration() - if err != nil { - log.Fatalf("Could not get migration: %s", err) - } - if db, err = postgres.Setup(dbConfig, *migration); err != nil { - log.Fatalf("Could not setup test DB connection: %s", err) - } - - database = postgres.NewDatabase(db, dbConfig, tracer) - - code := m.Run() - - db.Close() - if err := pool.Purge(container); err != nil { - log.Fatalf("Could not purge container: %s", err) - } - - os.Exit(code) -} diff --git a/reports/reports.go b/reports/reports.go deleted file mode 100644 index 4e19e151c..000000000 --- a/reports/reports.go +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "context" - "encoding/json" - "fmt" - "net/mail" - "strings" - "time" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/reltime" - "github.com/absmach/magistrala/pkg/schedule" - "github.com/absmach/magistrala/pkg/transformers/senml" -) - -var ( - errFromTimeNotProvided = errors.New("\"from time\" not provided") - errInvalidFromTime = errors.New("invalid \"from time\" ") - errToTimeNotProvided = errors.New("\"to time\" not provided") - errTitleNotProvided = errors.New("title not provided") - errInvalidToTime = errors.New("invalid \"to time\"") - errAggIntervalTimeNotProvided = errors.New("aggregation interval time not provided") - errInvalidAggInterval = errors.New("invalid aggregation interval time") - errNoToEmail = errors.New("no \"To\" email address found") - errChannelIDNotProvided = errors.New("channel id not provided") - errNameNotProvided = errors.New("name not provided") -) - -const ( - errInvalidFormatFmt = "invalid format %s" - errInvalidReportActionFmt = "invalid action %s" - errInvalidToEmail = "invalid \"To\" email %s" - - errUnknownAggregationFmt = "unknown aggregation type %d" - errUnknownAggregationStringFmt = "unknown aggregation type %s" -) - -type Report struct { - Metric Metric `json:"metric,omitempty"` - Messages []senml.Message `json:"messages,omitempty"` -} - -type ReportPage struct { - Total uint64 `json:"total"` - From time.Time `json:"from,omitempty"` - To time.Time `json:"to,omitempty"` - Aggregation AggConfig `json:"aggregation,omitempty"` - Reports []Report `json:"reports,omitempty"` - File ReportFile `json:"file,omitempty"` -} - -type ReportFile struct { - Name string `json:"name,omitempty"` - Data []byte `json:"data,omitempty"` - Format Format `json:"format,omitempty"` -} - -type AggConfig struct { - AggType Aggregation `json:"agg_type,omitempty"` // Optional field - Interval string `json:"interval,omitempty"` // Mandatory field if "AggType" field is set MAX, MIN, COUNT, SUM, AVG -} - -func (ac AggConfig) Validate() error { - if ac.AggType != AggregationNONE { - if ac.Interval == "" { - return errAggIntervalTimeNotProvided - } - - if _, err := time.ParseDuration(ac.Interval); err != nil { - return errInvalidAggInterval - } - } - return nil -} - -type MetricConfig struct { - From string `json:"from,omitempty"` // Mandatory field - To string `json:"to,omitempty"` // Mandatory field - Title string `json:"title,omitempty"` // Mandatory field - - FileFormat Format `json:"file_format"` // Optional field - Timezone string `json:"timezone,omitempty"` // Optional field, defaults to UTC - - Aggregation AggConfig `json:"aggregation,omitempty"` // Optional field -} - -func (mc MetricConfig) Validate() error { - if mc.From == "" { - return errFromTimeNotProvided - } - - if _, err := reltime.Parse(mc.From); err != nil { - return errInvalidFromTime - } - - if mc.To == "" { - return errToTimeNotProvided - } - - if _, err := reltime.Parse(mc.To); err != nil { - return errInvalidToTime - } - - if mc.Title == "" { - return errTitleNotProvided - } - - if err := mc.Aggregation.Validate(); err != nil { - return err - } - - if tz := strings.TrimSpace(mc.Timezone); tz != "" { - if _, err := time.LoadLocation(tz); err != nil { - return errors.Wrap(fmt.Errorf("invalid timezone: %s", tz), err) - } - } - - return nil -} - -type Metric struct { - ChannelID string `json:"channel_id,omitempty"` // Mandatory field - ClientID string `json:"client_id,omitempty"` // Optional field - Name string `json:"name,omitempty"` // Mandatory field - Subtopic string `json:"subtopic,omitempty"` // Optional field - Protocol string `json:"protocol,omitempty"` // Optional field - Format string `json:"format,omitempty"` // Optional field -} - -type ReqMetric struct { - ChannelID string `json:"channel_id,omitempty"` // Mandatory field - ClientIDs []string `json:"client_ids,omitempty"` // Optional field - Name string `json:"name,omitempty"` // Mandatory field - Subtopic string `json:"subtopic,omitempty"` // Optional field - Protocol string `json:"protocol,omitempty"` // Optional field - Format string `json:"format,omitempty"` // Optional field -} - -func (rm ReqMetric) Validate() error { - if rm.ChannelID == "" { - return errChannelIDNotProvided - } - if rm.Name == "" { - return errNameNotProvided - } - return nil -} - -type ReportConfig struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - DomainID string `json:"domain_id"` - Schedule schedule.Schedule `json:"schedule,omitempty"` - Config *MetricConfig `json:"config,omitempty"` - Email *EmailSetting `json:"email,omitempty"` - Metrics []ReqMetric `json:"metrics,omitempty"` - ReportTemplate ReportTemplate `json:"report_template,omitempty"` - Status Status `json:"status"` - CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by,omitempty"` - UpdatedAt time.Time `json:"updated_at"` - UpdatedBy string `json:"updated_by,omitempty"` -} - -type ReportConfigPage struct { - PageMeta - ReportConfigs []ReportConfig `json:"report_configs"` -} - -type EmailSetting struct { - To []string `json:"to,omitempty"` - Subject string `json:"subject,omitempty"` - Content string `json:"content,omitempty"` -} - -func (es *EmailSetting) Validate() error { - if len(es.To) == 0 { - return errNoToEmail - } - for _, to := range es.To { - if _, err := mail.ParseAddress(to); err != nil { - return errors.Wrap(fmt.Errorf(errInvalidToEmail, to), err) - } - } - return nil -} - -type Format uint8 - -const ( - PDF = iota - CSV - AllFormats -) - -const ( - PdfFormat = "pdf" - CsvFormat = "csv" - All_Formats = "AllFormats" -) - -func (f Format) String() string { - switch f { - case PDF: - return PdfFormat - case CSV: - return CsvFormat - case AllFormats: - return All_Formats - default: - return Unknown - } -} - -func (f Format) Extension() string { - switch f { - case PDF: - return PdfFormat - case CSV: - return CsvFormat - default: - return Unknown - } -} - -func (f Format) ContentType() string { - switch f { - case PDF: - return "application/pdf" - case CSV: - return "text/csv" - default: - return Unknown - } -} - -func ToFormat(format string) (Format, error) { - switch format { - case "", PdfFormat: - return PDF, nil - case CsvFormat: - return CSV, nil - case All_Formats: - return AllFormats, nil - } - return Format(0), fmt.Errorf(errInvalidFormatFmt, format) -} - -func (f Format) MarshalJSON() ([]byte, error) { - return json.Marshal(f.String()) -} - -func (f *Format) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToFormat(str) - *f = val - return err -} - -type ReportAction uint8 - -const ( - ViewReport = iota - DownloadReport - EmailReport -) - -const ( - ViewReportAction = "view" - DownloadReportAction = "download" - EmailReportAction = "email" -) - -func (ra ReportAction) String() string { - switch ra { - case ViewReport: - return ViewReportAction - case DownloadReport: - return DownloadReportAction - case EmailReport: - return EmailReportAction - default: - return Unknown - } -} - -func ToReportAction(action string) (ReportAction, error) { - switch action { - case "", ViewReportAction: - return ViewReport, nil - case DownloadReportAction: - return DownloadReport, nil - case EmailReportAction: - return EmailReport, nil - } - return ReportAction(0), fmt.Errorf(errInvalidReportActionFmt, action) -} - -func (ra ReportAction) MarshalJSON() ([]byte, error) { - return json.Marshal(ra.String()) -} - -func (ra *ReportAction) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToReportAction(str) - *ra = val - return err -} - -type Aggregation uint8 - -const ( - AggregationNONE = iota - AggregationMAX - AggregationMIN - AggregationSUM - AggregationCOUNT - AggregationAVG -) - -const ( - aggregationNONE = "none" - aggregationMAX = "max" - aggregationMIN = "min" - aggregationSUM = "sum" - aggregationCOUNT = "count" - aggregationAVG = "avg" -) - -func (a Aggregation) String() string { - switch a { - case AggregationNONE: - return aggregationNONE - case AggregationMAX: - return aggregationMAX - case AggregationMIN: - return aggregationMIN - case AggregationSUM: - return aggregationSUM - case AggregationCOUNT: - return aggregationCOUNT - case AggregationAVG: - return aggregationAVG - default: - return fmt.Sprintf(errUnknownAggregationFmt, a) - } -} - -func ToAggregation(agg string) (Aggregation, error) { - switch strings.ToLower(agg) { - case "", aggregationNONE: - return AggregationNONE, nil - case aggregationMAX: - return AggregationMAX, nil - case aggregationMIN: - return AggregationMIN, nil - case aggregationSUM: - return AggregationSUM, nil - case aggregationCOUNT: - return AggregationCOUNT, nil - case aggregationAVG: - return AggregationAVG, nil - default: - return Aggregation(0), fmt.Errorf(errUnknownAggregationStringFmt, agg) - } -} - -func (a Aggregation) MarshalJSON() ([]byte, error) { - return json.Marshal(a.String()) -} - -func (a *Aggregation) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToAggregation(str) - *a = val - return err -} - -type PageMeta struct { - Total uint64 `json:"total" db:"total"` - Offset uint64 `json:"offset" db:"offset"` - Limit uint64 `json:"limit" db:"limit"` - Name string `json:"name" db:"name"` - Dir string `json:"dir" db:"dir"` - Order string `json:"order" db:"order"` - Status Status `json:"status,omitempty" db:"status"` - Domain string `json:"domain_id,omitempty" db:"domain_id"` - ScheduledBefore *time.Time `json:"scheduled_before,omitempty" db:"scheduled_before"` // Filter rules scheduled before this time - ScheduledAfter *time.Time `json:"scheduled_after,omitempty" db:"scheduled_after"` // Filter rules scheduled after this time - UserID string `json:"user_id,omitempty" db:"user_id"` -} - -type Repository interface { - AddReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error) - ViewReportConfig(ctx context.Context, id string) (ReportConfig, error) - UpdateReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error) - UpdateReportSchedule(ctx context.Context, cfg ReportConfig) (ReportConfig, error) - RemoveReportConfig(ctx context.Context, id string) error - UpdateReportConfigStatus(ctx context.Context, cfg ReportConfig) (ReportConfig, error) - ListAllReportsConfig(ctx context.Context, pm PageMeta) (ReportConfigPage, error) - UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, error) - - UpdateReportTemplate(ctx context.Context, domainID, reportID string, template ReportTemplate) error - ViewReportTemplate(ctx context.Context, domainID, reportID string) (ReportTemplate, error) - DeleteReportTemplate(ctx context.Context, domainID, reportID string) error -} - -type Service interface { - AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) - ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (ReportConfig, error) - UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) - UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) - RemoveReportConfig(ctx context.Context, session authn.Session, id string) error - ListReportsConfig(ctx context.Context, session authn.Session, pm PageMeta) (ReportConfigPage, error) - EnableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) - DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) - - UpdateReportTemplate(ctx context.Context, session authn.Session, cfg ReportConfig) error - ViewReportTemplate(ctx context.Context, session authn.Session, id string) (ReportTemplate, error) - DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error - - GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error) - StartScheduler(ctx context.Context) error -} diff --git a/reports/service.go b/reports/service.go deleted file mode 100644 index af900b64d..000000000 --- a/reports/service.go +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "context" - "fmt" - "log/slog" - "strings" - "time" - - "github.com/absmach/magistrala" - grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/emailer" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkglog "github.com/absmach/magistrala/pkg/logger" - "github.com/absmach/magistrala/pkg/reltime" - "github.com/absmach/magistrala/pkg/ticker" - "github.com/absmach/magistrala/pkg/transformers/senml" -) - -const limit = 1000 - -type report struct { - repo Repository - runInfo chan pkglog.RunInfo - idp magistrala.IDProvider - email emailer.Emailer - ticker ticker.Ticker - readers grpcReadersV1.ReadersServiceClient - defaultTemplate ReportTemplate - converterURL string -} - -func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp magistrala.IDProvider, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient, template ReportTemplate, converterURL string) (Service, error) { - return &report{ - repo: repo, - idp: idp, - runInfo: runInfo, - email: emailer, - ticker: tck, - readers: readers, - defaultTemplate: template, - converterURL: converterURL, - }, nil -} - -func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (retCfg ReportConfig, retErr error) { - id, err := r.idp.ID() - if err != nil { - return ReportConfig{}, err - } - - now := time.Now().UTC() - cfg.ID = id - cfg.CreatedAt = now - cfg.CreatedBy = session.UserID - cfg.DomainID = session.DomainID - cfg.Status = EnabledStatus - - if cfg.Schedule.StartDateTime.IsZero() { - cfg.Schedule.StartDateTime = now - } - cfg.Schedule.Time = cfg.Schedule.StartDateTime - - reportConfig, err := r.repo.AddReportConfig(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - defer func() { - if retErr != nil { - if errRollBack := r.repo.RemoveReportConfig(ctx, reportConfig.ID); errRollBack != nil { - retErr = errors.Wrap(retErr, errors.Wrap(svcerr.ErrRollbackRepo, errRollBack)) - } - } - }() - - return reportConfig, nil -} - -func (r *report) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (ReportConfig, error) { - cfg, err := r.repo.ViewReportConfig(ctx, id) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - return cfg, nil -} - -func (r *report) UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - cfg.UpdatedAt = time.Now().UTC() - cfg.UpdatedBy = session.UserID - reportConfig, err := r.repo.UpdateReportConfig(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return reportConfig, nil -} - -func (r *report) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - cfg.UpdatedAt = time.Now().UTC() - cfg.UpdatedBy = session.UserID - cfg.Schedule.Time = cfg.Schedule.StartDateTime - c, err := r.repo.UpdateReportSchedule(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return c, nil -} - -func (r *report) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - if err := r.repo.RemoveReportConfig(ctx, id); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - return nil -} - -func (r *report) ListReportsConfig(ctx context.Context, session authn.Session, pm PageMeta) (ReportConfigPage, error) { - pm.Domain = session.DomainID - page, err := r.repo.ListAllReportsConfig(ctx, pm) - if err != nil { - return ReportConfigPage{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - return page, nil -} - -func (r *report) EnableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) { - status, err := ToStatus(Enabled) - if err != nil { - return ReportConfig{}, err - } - cfg := ReportConfig{ - ID: id, - UpdatedAt: time.Now().UTC(), - UpdatedBy: session.UserID, - Status: status, - } - cfg, err = r.repo.UpdateReportConfigStatus(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return cfg, nil -} - -func (r *report) DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) { - status, err := ToStatus(Disabled) - if err != nil { - return ReportConfig{}, err - } - cfg := ReportConfig{ - ID: id, - UpdatedAt: time.Now().UTC(), - UpdatedBy: session.UserID, - Status: status, - } - cfg, err = r.repo.UpdateReportConfigStatus(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return cfg, nil -} - -func (r *report) GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error) { - config.DomainID = session.DomainID - - if action != ViewReport && action != DownloadReport && action != EmailReport { - if config.Status != EnabledStatus { - return ReportPage{}, svcerr.ErrInvalidStatus - } - } - - reportPage, err := r.generateReport(ctx, config, action) - if err != nil { - return ReportPage{}, err - } - - return reportPage, nil -} - -func (r *report) generateReport(ctx context.Context, cfg ReportConfig, action ReportAction) (ReportPage, error) { - genReportFile, err := r.generateFileFunc(ctx, action, cfg.Config.FileFormat, cfg.ReportTemplate, cfg.Config.Timezone) - if err != nil { - return ReportPage{}, err - } - - agg := grpcReadersV1.Aggregation_AGGREGATION_UNSPECIFIED - switch cfg.Config.Aggregation.AggType { - case AggregationMAX: - agg = grpcReadersV1.Aggregation_AGGREGATION_MAX - case AggregationMIN: - agg = grpcReadersV1.Aggregation_AGGREGATION_MIN - case AggregationCOUNT: - agg = grpcReadersV1.Aggregation_AGGREGATION_COUNT - case AggregationAVG: - agg = grpcReadersV1.Aggregation_AGGREGATION_AVG - case AggregationSUM: - agg = grpcReadersV1.Aggregation_AGGREGATION_SUM - } - - loc, err := resolveTimezone(cfg.Config.Timezone) - if err != nil { - r.runInfo <- pkglog.RunInfo{ - Level: slog.LevelWarn, - Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", cfg.Config.Timezone, err), - Details: []slog.Attr{ - slog.String("report_name", cfg.Name), - slog.String("timezone", cfg.Config.Timezone), - }, - } - } - - from, err := reltime.Parse(cfg.Config.From) - if err != nil { - return ReportPage{}, err - } - - to, err := reltime.Parse(cfg.Config.To) - if err != nil { - return ReportPage{}, err - } - - fromDisplay := from.In(loc) - toDisplay := to.In(loc) - - pm := &grpcReadersV1.PageMetadata{ - Aggregation: agg, - Limit: limit, - From: float64(from.UnixNano()), - To: float64(to.UnixNano()), - Interval: cfg.Config.Aggregation.Interval, - } - - var mets []Metric - var reports []Report - for _, metric := range cfg.Metrics { - switch { - case len(metric.ClientIDs) != 0: - for _, clientID := range metric.ClientIDs { - mets = append(mets, Metric{ - ChannelID: metric.ChannelID, - ClientID: clientID, - Name: metric.Name, - Subtopic: metric.Subtopic, - Protocol: metric.Protocol, - Format: metric.Format, - }) - } - default: - mets = append(mets, Metric{ - ChannelID: metric.ChannelID, - Name: metric.Name, - Subtopic: metric.Subtopic, - Protocol: metric.Protocol, - Format: metric.Format, - }) - } - } - - for _, metric := range mets { - sMsgs := []senml.Message{} - - pm.Offset = uint64(0) - pm.Name = metric.Name - if metric.ClientID != "" { - pm.Publisher = metric.ClientID - } - if metric.Subtopic != "" { - pm.Subtopic = metric.Subtopic - } - if metric.Protocol != "" { - pm.Protocol = metric.Protocol - } - if metric.Format != "" { - pm.Format = metric.Format - } - - msgs, err := r.readers.ReadMessages(ctx, &grpcReadersV1.ReadMessagesReq{ - ChannelId: metric.ChannelID, - DomainId: cfg.DomainID, - PageMetadata: pm, - }) - if err != nil { - return ReportPage{}, err - } - for _, msg := range msgs.Messages { - sMsgs = append(sMsgs, convertToSenml(msg.GetSenml())) - } - - for msgs.GetTotal() > (pm.Offset + pm.Limit) { - pm.Offset = pm.Offset + pm.Limit - msgs, err := r.readers.ReadMessages(ctx, &grpcReadersV1.ReadMessagesReq{ - ChannelId: metric.ChannelID, - DomainId: cfg.DomainID, - PageMetadata: pm, - }) - if err != nil { - return ReportPage{}, err - } - for _, msg := range msgs.Messages { - sMsgs = append(sMsgs, convertToSenml(msg.GetSenml())) - } - } - - reports = append(reports, convertToReports(metric, sMsgs)...) - } - - switch { - case genReportFile != nil: - data, err := genReportFile(ctx, cfg.Config.Title, reports) - if err != nil { - return ReportPage{}, err - } - timeStr := strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "") - filePrefix := cfg.Name - if filePrefix == "" { - filePrefix = "report" - } - fileName := fmt.Sprintf("%s_%s.%s", filePrefix, timeStr, cfg.Config.FileFormat.Extension()) - - file := ReportFile{ - Name: fileName, - Data: data, - Format: cfg.Config.FileFormat, - } - - switch action { - case EmailReport: - if err := r.emailReports(*cfg.Email, file); err != nil { - return ReportPage{}, errors.Wrap(err, svcerr.ErrCreateEntity) - } - - return ReportPage{}, nil - default: - return ReportPage{ - File: file, - }, nil - } - - default: - return ReportPage{ - From: fromDisplay, - To: toDisplay, - Aggregation: cfg.Config.Aggregation, - Total: uint64(len(reports)), - Reports: reports, - }, nil - } -} - -func (r *report) generateFileFunc(_ context.Context, action ReportAction, format Format, customTemplate ReportTemplate, timezone string) (func(context.Context, string, []Report) ([]byte, error), error) { - switch action { - case DownloadReport, EmailReport: - switch format { - case PDF: - return func(ctx context.Context, title string, reports []Report) ([]byte, error) { - return r.generatePDFReport(ctx, title, reports, customTemplate, timezone) - }, nil - case CSV: - return func(ctx context.Context, title string, reports []Report) ([]byte, error) { - return r.generateCSVReport(ctx, title, reports, timezone) - }, nil - default: - return nil, errors.New("file format not supported") - } - default: - return nil, nil - } -} - -func (r *report) emailReports(es EmailSetting, file ReportFile) error { - if err := es.Validate(); err != nil { - return errors.Wrap(svcerr.ErrMalformedEntity, err) - } - - attachments := map[string][]byte{ - file.Name: file.Data, - } - - if err := r.email.SendEmailNotification( - es.To, - "", - es.Subject, - "", - "", - es.Content, - "", - attachments, - ); err != nil { - return err - } - return nil -} - -func convertToSenml(g *grpcReadersV1.SenMLMessage) senml.Message { - if g == nil { - return senml.Message{} - } - return senml.Message{ - Protocol: g.Base.GetProtocol(), - Subtopic: g.Base.GetSubtopic(), - Publisher: g.Base.GetPublisher(), - Channel: g.Base.GetChannel(), - Name: g.GetName(), - Unit: g.GetUnit(), - Time: g.GetTime(), - UpdateTime: g.GetUpdateTime(), - Value: g.Value, - StringValue: g.StringValue, - DataValue: g.DataValue, - BoolValue: g.BoolValue, - Sum: g.Sum, - } -} - -func convertToReports(metric Metric, senmlMsgs []senml.Message) []Report { - if metric.ClientID != "" { - return []Report{ - { - Metric: metric, - Messages: senmlMsgs, - }, - } - } - - return groupReportsByPublisher(metric, senmlMsgs) -} - -func groupReportsByPublisher(metric Metric, sMsgs []senml.Message) []Report { - publishers := map[string][]senml.Message{} - - for _, msg := range sMsgs { - publishers[msg.Publisher] = append(publishers[msg.Publisher], msg) - } - - var groupedReports []Report - for publisher, messages := range publishers { - gMetric := metric - gMetric.ClientID = publisher - groupedReports = append(groupedReports, Report{ - Metric: gMetric, - Messages: messages, - }) - } - - if len(groupedReports) == 0 { - groupedReports = append(groupedReports, Report{ - Metric: metric, - Messages: []senml.Message{}, - }) - } - - return groupedReports -} - -func (r *report) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg ReportConfig) error { - err := r.repo.UpdateReportTemplate(ctx, session.DomainID, cfg.ID, cfg.ReportTemplate) - if err != nil { - return errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return nil -} - -func (r *report) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (ReportTemplate, error) { - template, err := r.repo.ViewReportTemplate(ctx, session.DomainID, id) - if err != nil { - return "", errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return template, nil -} - -func (r *report) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { - err := r.repo.DeleteReportTemplate(ctx, session.DomainID, id) - if err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - return nil -} diff --git a/reports/service_test.go b/reports/service_test.go deleted file mode 100644 index 175d220b5..000000000 --- a/reports/service_test.go +++ /dev/null @@ -1,650 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/authn" - emocks "github.com/absmach/magistrala/pkg/emailer/mocks" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - pkglog "github.com/absmach/magistrala/pkg/logger" - policymocks "github.com/absmach/magistrala/pkg/policies/mocks" - pkgSch "github.com/absmach/magistrala/pkg/schedule" - tmocks "github.com/absmach/magistrala/pkg/ticker/mocks" - "github.com/absmach/magistrala/pkg/uuid" - readmocks "github.com/absmach/magistrala/readers/mocks" - "github.com/absmach/magistrala/reports" - "github.com/absmach/magistrala/reports/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - namegen = namegenerator.NewGenerator() - userID = testsutil.GenerateUUID(&testing.T{}) - domainID = testsutil.GenerateUUID(&testing.T{}) - now = time.Now().UTC() - template = reports.ReportTemplate("") - schedule = pkgSch.Schedule{ - StartDateTime: now, - Recurring: pkgSch.Daily, - RecurringPeriod: 1, - Time: time.Now().Add(-time.Hour), - } - reportName = namegen.Generate() - rptConfig = reports.ReportConfig{ - ID: testsutil.GenerateUUID(&testing.T{}), - Name: reportName, - DomainID: domainID, - Status: reports.EnabledStatus, - Schedule: schedule, - CreatedBy: userID, - UpdatedBy: userID, - UpdatedAt: time.Now(), - } -) - -func newService(t *testing.T, runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *tmocks.Ticker, *policymocks.Service) { - repo := new(mocks.Repository) - mockTicker := new(tmocks.Ticker) - idProvider := uuid.NewMock() - readersSvc := new(readmocks.ReadersServiceClient) - e := new(emocks.Emailer) - policy := new(policymocks.Service) - - svc, err := reports.NewService(repo, runInfo, idProvider, mockTicker, e, readersSvc, template, "") - if err != nil { - t.Fatalf("Failed to create service: %v", err) - } - return svc, repo, mockTicker, policy -} - -func TestAddReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - res reports.ReportConfig - err error - }{ - { - desc: "Add report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: reports.ReportConfig{ - Name: reportName, - Schedule: schedule, - }, - res: rptConfig, - err: nil, - }, - { - desc: "Add report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: reports.ReportConfig{ - Name: reportName, - Schedule: schedule, - }, - err: repoerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("AddReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.AddReportConfig(context.Background(), tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.NotEmpty(t, res.ID, "expected non-empty result in ID") - assert.Equal(t, tc.cfg.Name, res.Name) - assert.Equal(t, tc.cfg.Schedule, res.Schedule) - } - repoCall.Unset() - }) - } -} - -func TestAddReportConfigWithoutRoleProvisioning(t *testing.T) { - repo := new(mocks.Repository) - mockTicker := new(tmocks.Ticker) - idProvider := uuid.NewMock() - readersSvc := new(readmocks.ReadersServiceClient) - e := new(emocks.Emailer) - - svc, err := reports.NewService(repo, make(chan pkglog.RunInfo), idProvider, mockTicker, e, readersSvc, template, "") - if err != nil { - t.Fatalf("Failed to create service: %v", err) - } - - session := authn.Session{ - UserID: userID, - DomainID: domainID, - } - cfg := reports.ReportConfig{ - Name: reportName, - Schedule: schedule, - } - saved := cfg - saved.ID = rptConfig.ID - saved.Status = reports.EnabledStatus - saved.CreatedBy = userID - saved.DomainID = domainID - - repo.On("AddReportConfig", mock.Anything, mock.Anything).Return(saved, nil).Once() - - res, err := svc.AddReportConfig(context.Background(), session, cfg) - assert.NoError(t, err) - assert.Equal(t, saved.ID, res.ID) - repo.AssertNotCalled(t, "AddRoles", mock.Anything, mock.Anything) - repo.AssertExpectations(t) -} - -func TestViewReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - res reports.ReportConfig - err error - }{ - { - desc: "view report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - res: rptConfig, - err: nil, - }, - { - desc: "view report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("ViewReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.ViewReportConfig(context.Background(), tc.session, tc.id, false) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestUpdateReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - - newName := namegen.Generate() - now := time.Now().Add(time.Hour) - cases := []struct { - desc string - session authn.Session - cfg reports.ReportConfig - res reports.ReportConfig - err error - }{ - { - desc: "update report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: reports.ReportConfig{ - Name: newName, - ID: rptConfig.ID, - Schedule: schedule, - }, - res: reports.ReportConfig{ - Name: newName, - ID: rptConfig.ID, - DomainID: rptConfig.DomainID, - Status: rptConfig.Status, - Schedule: rptConfig.Schedule, - UpdatedAt: now, - UpdatedBy: userID, - }, - err: nil, - }, - { - desc: "update report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: reports.ReportConfig{ - Name: rptConfig.Name, - ID: rptConfig.ID, - Schedule: schedule, - }, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.UpdateReportConfig(context.Background(), tc.session, tc.cfg) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestListReportsConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - numConfigs := 50 - now := time.Now().Add(time.Hour) - var configs []reports.ReportConfig - for i := 0; i < numConfigs; i++ { - c := reports.ReportConfig{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: reports.EnabledStatus, - CreatedAt: now, - CreatedBy: userID, - Schedule: schedule, - } - configs = append(configs, c) - } - - cases := []struct { - desc string - session authn.Session - pageMeta reports.PageMeta - res reports.ReportConfigPage - err error - superAdmin bool - }{ - { - desc: "list report configs successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: reports.PageMeta{}, - res: reports.ReportConfigPage{ - PageMeta: reports.PageMeta{ - Total: uint64(numConfigs), - Offset: 0, - Limit: 10, - }, - ReportConfigs: configs[0:10], - }, - err: nil, - }, - { - desc: "list report configs successfully with limit", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: reports.PageMeta{ - Limit: 100, - }, - res: reports.ReportConfigPage{ - PageMeta: reports.PageMeta{ - Total: uint64(numConfigs), - Offset: 0, - Limit: 100, - }, - ReportConfigs: configs[0:numConfigs], - }, - err: nil, - }, - { - desc: "list report configs successfully with offset", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: reports.PageMeta{ - Offset: 20, - Limit: 10, - }, - res: reports.ReportConfigPage{ - PageMeta: reports.PageMeta{ - Total: uint64(numConfigs), - Offset: 20, - Limit: 10, - }, - ReportConfigs: configs[20:30], - }, - err: nil, - }, - { - desc: "list report configs with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: reports.PageMeta{}, - err: svcerr.ErrViewEntity, - }, - { - desc: "list report configs as super admin successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - SuperAdmin: true, - }, - pageMeta: reports.PageMeta{}, - res: reports.ReportConfigPage{ - PageMeta: reports.PageMeta{ - Total: uint64(numConfigs), - Offset: 0, - Limit: 10, - }, - ReportConfigs: configs[0:10], - }, - superAdmin: true, - err: nil, - }, - { - desc: "list report configs as super admin with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - SuperAdmin: true, - }, - pageMeta: reports.PageMeta{}, - superAdmin: true, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - var repoCall *mock.Call - repoCall = repo.On("ListAllReportsConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.ListReportsConfig(context.Background(), tc.session, tc.pageMeta) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestRemoveReportConfig(t *testing.T) { - svc, repo, _, policies := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - err error - deletePoliciesErr error - }{ - { - desc: "remove report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - err: nil, - deletePoliciesErr: nil, - }, - { - desc: "remove report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - err: svcerr.ErrRemoveEntity, - deletePoliciesErr: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err) - policyCall := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePoliciesErr) - err := svc.RemoveReportConfig(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - policyCall.Unset() - repoCall.Unset() - }) - } -} - -func TestEnableReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - status reports.Status - res reports.ReportConfig - err error - }{ - { - desc: "enable report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: reports.EnabledStatus, - res: rptConfig, - err: nil, - }, - { - desc: "enable report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: reports.EnabledStatus, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateReportConfigStatus", context.Background(), mock.Anything).Return(tc.res, tc.err) - res, err := svc.EnableReportConfig(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestDisableReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - status reports.Status - res reports.ReportConfig - err error - }{ - { - desc: "disable report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: reports.DisabledStatus, - res: reports.ReportConfig{ - ID: rptConfig.ID, - Name: rptConfig.Name, - DomainID: rptConfig.DomainID, - Status: reports.DisabledStatus, - Schedule: schedule, - UpdatedBy: userID, - UpdatedAt: time.Now(), - }, - err: nil, - }, - { - desc: "disable report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: reports.DisabledStatus, - err: svcerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("UpdateReportConfigStatus", mock.Anything, mock.Anything).Return(tc.res, tc.err) - res, err := svc.DisableReportConfig(context.Background(), tc.session, tc.id) - - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.res, res) - } - defer repoCall.Unset() - }) - } -} - -func TestGenerateInstantEmailReport(t *testing.T) { - // nolint:dogsled - svc, _, _, _ := newService(t, make(chan pkglog.RunInfo)) - - validEmailConfig := reports.EmailSetting{ - To: []string{"test@example.com"}, - Subject: "Test Report", - Content: "Please find the attached report.", - } - - validConfig := reports.ReportConfig{ - ID: testsutil.GenerateUUID(&testing.T{}), - Name: "Test Report", - DomainID: domainID, - Status: reports.DisabledStatus, - Email: &validEmailConfig, - Config: &reports.MetricConfig{ - Title: "Test Report", - FileFormat: reports.PDF, - From: "now-1h", - To: "now", - Aggregation: reports.AggConfig{ - AggType: reports.AggregationAVG, - Interval: "1h", - }, - }, - Metrics: []reports.ReqMetric{ - { - ChannelID: testsutil.GenerateUUID(&testing.T{}), - Name: "temperature", - ClientIDs: []string{testsutil.GenerateUUID(&testing.T{})}, - }, - }, - ReportTemplate: template, - } - - cases := []struct { - desc string - session authn.Session - config reports.ReportConfig - action reports.ReportAction - err error - }{ - { - desc: "Generate instant email report with disabled config should succeed", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - config: validConfig, - action: reports.EmailReport, - err: nil, - }, - { - desc: "Generate instant email report with enabled config should succeed", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - config: func() reports.ReportConfig { - cfg := validConfig - cfg.Status = reports.EnabledStatus - return cfg - }(), - action: reports.EmailReport, - err: nil, - }, - { - desc: "Generate view report with disabled config should succeed", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - config: validConfig, - action: reports.ViewReport, - err: nil, - }, - { - desc: "Generate download report with disabled config should succeed", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - config: validConfig, - action: reports.DownloadReport, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - _, err := svc.GenerateReport(context.Background(), tc.session, tc.config, tc.action) - - if tc.err != nil { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } else { - assert.False(t, errors.Contains(err, svcerr.ErrInvalidStatus), fmt.Sprintf("%s: should not get ErrInvalidStatus for instant reports, got %s\n", tc.desc, err)) - } - }) - } -} diff --git a/reports/status.go b/reports/status.go deleted file mode 100644 index 1c53778d6..000000000 --- a/reports/status.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "encoding/json" - "strings" - - svcerr "github.com/absmach/magistrala/pkg/errors/service" -) - -// Status represents Rule status. -type Status uint8 - -// Possible User status values. -const ( - // EnabledStatus represents enabled Rule. - EnabledStatus Status = iota - // DisabledStatus represents disabled Rule. - DisabledStatus - // DeletedStatus represents a rule that will be deleted. - DeletedStatus - - // AllStatus is used for querying purposes to list rules irrespective - // of their status - both enabled and disabled. It is never stored in the - // database as the actual User status and should always be the largest - // value in this enumeration. - AllStatus -) - -// String representation of the possible status values. -const ( - Disabled = "disabled" - Enabled = "enabled" - Deleted = "deleted" - All = "all" - Unknown = "unknown" -) - -func (s Status) String() string { - switch s { - case DisabledStatus: - return Disabled - case EnabledStatus: - return Enabled - case DeletedStatus: - return Deleted - case AllStatus: - return All - default: - return Unknown - } -} - -// ToStatus converts string value to a valid status. -func ToStatus(status string) (Status, error) { - switch status { - case "", Enabled: - return EnabledStatus, nil - case Disabled: - return DisabledStatus, nil - case Deleted: - return DeletedStatus, nil - case All: - return AllStatus, nil - } - return Status(0), svcerr.ErrInvalidStatus -} - -func (s Status) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -func (s *Status) UnmarshalJSON(data []byte) error { - str := strings.Trim(string(data), "\"") - val, err := ToStatus(str) - *s = val - return err -} diff --git a/reports/template.go b/reports/template.go deleted file mode 100644 index 119117457..000000000 --- a/reports/template.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "encoding/json" - "fmt" - "text/template" - "text/template/parse" -) - -type ReportTemplate string - -func (temp ReportTemplate) String() string { - return string(temp) -} - -func (temp ReportTemplate) MarshalJSON() ([]byte, error) { - return json.Marshal(string(temp)) -} - -func (temp *ReportTemplate) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - *temp = ReportTemplate(s) - return nil -} - -func (temp ReportTemplate) Validate() error { - templateStr := string(temp) - - // Validate template syntax using Go's template parser - tmpl := template.New("validate").Funcs(template.FuncMap{ - "add": func(a, b int) int { return a + b }, - "sub": func(a, b int) int { return a - b }, - "div": func(a, b int) int { - if b == 0 { - return 0 - } - return a / b - }, - "mod": func(a, b int) int { - if b == 0 { - return 0 - } - return a % b - }, - "eq": func(a, b int) bool { return a == b }, - "ge": func(a, b int) bool { return a >= b }, - "lt": func(a, b int) bool { return a < b }, - "iterate": func(count int) []int { return make([]int, count) }, - "getStartRow": func(pageNum, firstPageRows, continuationPageRows int) int { return 0 }, - "getEndRow": func(pageNum, firstPageRows, continuationPageRows, totalMessages int) int { return 0 }, - templateFormatTime: func(t any) string { return "" }, - templateFormatValue: func(v any) string { return "" }, - }) - - parsed, err := tmpl.Parse(templateStr) - if err != nil { - return fmt.Errorf("template syntax error: %w", err) - } - - var hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd bool - // Validate essential fields are present using template parsing - if err := validateEssentialFields(parsed.Tree.Root, &hasTitle, &hasRange, &hasFormatTime, &hasFormatValue, &hasEnd); err != nil { - return err - } - - if !hasTitle { - return fmt.Errorf("missing essential template field: {{$.Title}}") - } - if !hasRange { - return fmt.Errorf("missing essential template field: {{range .Messages}} or {{range .Reports}}") - } - if !hasFormatTime { - return fmt.Errorf("missing essential template field: {{formatTime .Time}}") - } - if !hasFormatValue { - return fmt.Errorf("missing essential template field: {{formatValue .}}") - } - if !hasEnd { - return fmt.Errorf("missing essential template field: {{end}}") - } - - return nil -} - -func validateEssentialFields(node parse.Node, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd *bool) error { - if node == nil { - return nil - } - - switch n := node.(type) { - case *parse.ListNode: - for _, sub := range n.Nodes { - if err := validateEssentialFields(sub, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - } - - case *parse.ActionNode: - if n.Pipe != nil { - for _, cmd := range n.Pipe.Cmds { - cmdStr := cmd.String() - if cmdStr == "$.Title" { - *hasTitle = true - } - if len(cmd.Args) > 0 { - firstArg := cmd.Args[0].String() - if firstArg == "formatTime" { - *hasFormatTime = true - } - if firstArg == "formatValue" { - *hasFormatValue = true - } - } - } - } - - case *parse.RangeNode: - if n.Pipe != nil && len(n.Pipe.Cmds) > 0 { - cmdStr := n.Pipe.Cmds[0].String() - // Accept .Messages, .Reports, or $report.Messages - if cmdStr == ".Messages" || cmdStr == ".Reports" || cmdStr == "$report.Messages" { - *hasRange = true - } - } - if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - if n.ElseList != nil { - if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - } - *hasEnd = true - - case *parse.IfNode: - if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - if n.ElseList != nil { - if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - } - - case *parse.WithNode: - if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - if n.ElseList != nil { - if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil { - return err - } - } - } - - return nil -} diff --git a/reports/template_test.go b/reports/template_test.go deleted file mode 100644 index 69ab33a25..000000000 --- a/reports/template_test.go +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports_test - -import ( - "fmt" - "testing" - - "github.com/absmach/magistrala/reports" - "github.com/stretchr/testify/assert" -) - -const ( - validTemplate = ` - - - {{$.Title}} - - - -
-

{{$.Title}}

-

Generated on: {{$.GeneratedDate}}

-
-
-

Messages

- {{range .Messages}} -
-

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

-
- {{end}} -
- -` - - templateWithoutTitle = ` - - - Report - - - -

Report

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end}} - -` - - templateWithoutRange = ` - - - {{$.Title}} - - -

{{$.Title}}

-

No messages to display

- -` - - templateWithoutFormatTime = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{range .Messages}} -

Time: {{.Time}}

-

Value: {{formatValue .}}

- {{end}} - -` - - templateWithoutFormatValue = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{.}}

- {{end}} - -` - - templateWithoutEnd = ` - - - {{$.Title}} - - -

{{$.Title}}

-

Time: {{formatTime "test"}}

-

Value: {{formatValue "test"}}

-

No range block with end

- -` - - templateWithSyntaxError = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end - -` - - templateWithUndefinedFunction = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

-

Custom: {{customFunction .}}

- {{end}} - -` - - templateWithIfCondition = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{if .Messages}} - {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end}} - {{else}} -

No messages available

- {{end}} - -` - - templateWithWithCondition = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{with .Data}} - {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end}} - {{else}} -

No data available

- {{end}} - -` - - templateWithNestedConditions = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{if .HasMessages}} - {{with .Data}} - {{range .Messages}} -

Time: {{formatTime .Time}}

-

Value: {{formatValue .}}

- {{end}} - {{else}} -

Data not available

- {{end}} - {{else}} -

No messages flag set

- {{end}} - -` - - templateWithIfMissingFields = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{if .Messages}} - {{range .Messages}} -

Time: {{.Time}}

-

Value: {{.}}

- {{end}} - {{else}} -

No messages available

- {{end}} - -` - - templateWithWithMissingFields = ` - - - {{$.Title}} - - -

{{$.Title}}

- {{with .Data}} - {{range .Messages}} -

Time: {{.Time}}

-

Value: {{formatValue .}}

- {{end}} - {{else}} -

No data available

- {{end}} - -` -) - -func TestReportTemplate_Validate(t *testing.T) { - cases := []struct { - desc string - template reports.ReportTemplate - err error - }{ - { - desc: "validate template successfully", - template: reports.ReportTemplate(validTemplate), - err: nil, - }, - { - desc: "validate template without title field", - template: reports.ReportTemplate(templateWithoutTitle), - err: fmt.Errorf("missing essential template field: {{$.Title}}"), - }, - { - desc: "validate template without range field", - template: reports.ReportTemplate(templateWithoutRange), - err: fmt.Errorf("missing essential template field: {{range .Messages}}"), - }, - { - desc: "validate template without formatTime field", - template: reports.ReportTemplate(templateWithoutFormatTime), - err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"), - }, - { - desc: "validate template without formatValue field", - template: reports.ReportTemplate(templateWithoutFormatValue), - err: fmt.Errorf("missing essential template field: {{formatValue .}}"), - }, - { - desc: "validate template without end field", - template: reports.ReportTemplate(templateWithoutEnd), - err: fmt.Errorf("missing essential template field: {{range .Messages}}"), - }, - { - desc: "validate template with syntax error", - template: reports.ReportTemplate(templateWithSyntaxError), - err: fmt.Errorf("template syntax error"), - }, - { - desc: "validate template with undefined function", - template: reports.ReportTemplate(templateWithUndefinedFunction), - err: fmt.Errorf("template syntax error"), - }, - { - desc: "validate empty template", - template: reports.ReportTemplate(""), - err: fmt.Errorf("missing essential template field: {{$.Title}}"), - }, - { - desc: "validate template with if condition successfully", - template: reports.ReportTemplate(templateWithIfCondition), - err: nil, - }, - { - desc: "validate template `with` with condition successfully", - template: reports.ReportTemplate(templateWithWithCondition), - err: nil, - }, - { - desc: "validate template with nested conditions successfully", - template: reports.ReportTemplate(templateWithNestedConditions), - err: nil, - }, - { - desc: "validate template with if condition missing formatTime", - template: reports.ReportTemplate(templateWithIfMissingFields), - err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"), - }, - { - desc: "validate template `with` with condition missing formatTime", - template: reports.ReportTemplate(templateWithWithMissingFields), - err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"), - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.template.Validate() - if tc.err != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.err.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestReportTemplate_String(t *testing.T) { - template := reports.ReportTemplate(validTemplate) - result := template.String() - - assert.Equal(t, validTemplate, result) -} - -func TestReportTemplate_MarshalJSON(t *testing.T) { - template := reports.ReportTemplate("simple template") - data, err := template.MarshalJSON() - - assert.NoError(t, err) - assert.NotNil(t, data) - assert.Equal(t, `"simple template"`, string(data)) -} - -func TestReportTemplate_UnmarshalJSON(t *testing.T) { - cases := []struct { - desc string - data []byte - expected string - err error - }{ - { - desc: "unmarshal valid JSON successfully", - data: []byte(`"simple template"`), - expected: "simple template", - err: nil, - }, - { - desc: "unmarshal invalid JSON", - data: []byte(`invalid json`), - err: fmt.Errorf("invalid character"), - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - var template reports.ReportTemplate - err := template.UnmarshalJSON(tc.data) - - if tc.err != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.err.Error()) - } else { - assert.NoError(t, err) - assert.Equal(t, tc.expected, string(template)) - } - }) - } -} diff --git a/reports/tz.go b/reports/tz.go deleted file mode 100644 index d2e317686..000000000 --- a/reports/tz.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package reports - -import ( - "strings" - "time" -) - -// resolveTimezone returns a *time.Location from a user-provided IANA timezone name. -// Supported inputs: -// - IANA names (e.g., "Europe/Paris", "America/New_York"). -// - Empty string defaults to UTC. -func resolveTimezone(s string) (*time.Location, error) { - s = strings.TrimSpace(s) - if s == "" { - return time.UTC, nil - } - loc, err := time.LoadLocation(s) - if err != nil { - return time.UTC, err - } - return loc, nil -} diff --git a/scripts/ci.sh b/scripts/ci.sh index c8ac5f61a..0dffbf371 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -69,8 +69,6 @@ setup_mg() { exit 1 fi done - echo "Compile check for nats message broker..." - MG_MESSAGE_BROKER_TYPE=msg_nats make re echo "Compile check for redis event store..." MG_ES_TYPE=es_redis make bootstrap make -j$NPROC diff --git a/tools/config/.mockery.yaml b/tools/config/.mockery.yaml index 44d7af2b3..a06886663 100644 --- a/tools/config/.mockery.yaml +++ b/tools/config/.mockery.yaml @@ -63,10 +63,6 @@ packages: interfaces: Service: SubscriptionsRepository: - github.com/absmach/magistrala/journal: - interfaces: - Repository: - Service: github.com/absmach/magistrala/pkg/authn: interfaces: Authentication: @@ -97,18 +93,3 @@ packages: github.com/absmach/magistrala/readers: interfaces: MessageRepository: - github.com/absmach/magistrala/re: - interfaces: - Repository: - Service: - github.com/absmach/magistrala/alarms: - interfaces: - Service: - Repository: - github.com/absmach/magistrala/reports: - interfaces: - Service: - Repository: - github.com/absmach/magistrala/notifications: - interfaces: - Notifier: