From dcd5ff914d58e8437edbe4f49cc73eb4450117a6 Mon Sep 17 00:00:00 2001 From: Steve Munene Date: Mon, 16 Jun 2025 13:10:50 +0300 Subject: [PATCH] MG-136 - Move reports to a separate service (#152) * initial implementation Signed-off-by: nyagamunene * initial implementation Signed-off-by: nyagamunene * add remove report from nats handler Signed-off-by: nyagamunene * add license header Signed-off-by: nyagamunene * fix failing linter Signed-off-by: nyagamunene * remove unused code Signed-off-by: nyagamunene * update docker compose Signed-off-by: nyagamunene * address comments Signed-off-by: nyagamunene * fix failing linter Signed-off-by: nyagamunene * move runinfo to pkg Signed-off-by: nyagamunene * update report handler Signed-off-by: nyagamunene * update reports handler Signed-off-by: nyagamunene * update handler in reports Signed-off-by: nyagamunene * update repo method from time to due Signed-off-by: nyagamunene * fix validation methods Signed-off-by: nyagamunene * address comments Signed-off-by: nyagamunene * update reports port to 9017 Signed-off-by: nyagamunene * update nginx to support reports Signed-off-by: nyagamunene * fix reports location in nginx Signed-off-by: nyagamunene * update env variable Signed-off-by: nyagamunene --------- Signed-off-by: nyagamunene --- Makefile | 2 +- apidocs/openapi/reports.yaml | 556 +++++++++++++++++++ cmd/re/main.go | 10 +- cmd/reports/main.go | 258 +++++++++ docker/.env | 18 + docker/docker-compose.yaml | 91 ++++ docker/nginx/entrypoint.sh | 1 + docker/nginx/nginx-key.conf | 9 +- docker/nginx/nginx-x509.conf | 9 +- docker/templates/reports.tmpl | 3 + {re => pkg}/emailer/emailer.go | 10 +- pkg/logger/logger.go | 12 + {re => pkg/schedule}/schedule.go | 5 +- {re => pkg/ticker}/ticker.go | 2 +- re/api/endpoints.go | 224 -------- re/api/endpoints_test.go | 715 +----------------------- re/api/requests.go | 158 +----- re/api/responses.go | 147 ----- re/api/transport.go | 200 ------- re/emailer.go | 9 - re/emailer/doc.go | 6 - re/handlers.go | 50 +- re/middleware/authorization.go | 157 +----- re/middleware/logging.go | 171 ------ re/mocks/repository.go | 432 --------------- re/mocks/service.go | 496 ----------------- re/postgres/init.go | 26 - re/postgres/repository.go | 315 ----------- re/postgres/rule.go | 166 +----- re/rule.go | 40 +- re/service.go | 439 +-------------- re/service_test.go | 483 +---------------- reports/api/doc.go | 6 + reports/api/endpoints.go | 238 ++++++++ reports/api/endpoints_test.go | 813 ++++++++++++++++++++++++++++ reports/api/request.go | 171 ++++++ reports/api/response.go | 167 ++++++ reports/api/transport.go | 247 +++++++++ {re => reports}/generator.go | 2 +- reports/handler.go | 64 +++ reports/middleware/authorization.go | 190 +++++++ reports/middleware/logging.go | 210 +++++++ reports/mocks/repository.go | 475 ++++++++++++++++ reports/mocks/service.go | 584 ++++++++++++++++++++ reports/postgres/init.go | 42 ++ reports/postgres/reports.go | 137 +++++ reports/postgres/repository.go | 340 ++++++++++++ {re => reports}/reports.go | 67 ++- reports/service.go | 418 ++++++++++++++ reports/service_test.go | 466 ++++++++++++++++ reports/status.go | 80 +++ tools/config/.mockery.yaml | 4 + 52 files changed, 5789 insertions(+), 4152 deletions(-) create mode 100644 apidocs/openapi/reports.yaml create mode 100644 cmd/reports/main.go create mode 100644 docker/templates/reports.tmpl rename {re => pkg}/emailer/emailer.go (61%) create mode 100644 pkg/logger/logger.go rename {re => pkg/schedule}/schedule.go (96%) rename {re => pkg/ticker}/ticker.go (95%) delete mode 100644 re/emailer.go delete mode 100644 re/emailer/doc.go create mode 100644 reports/api/doc.go create mode 100644 reports/api/endpoints.go create mode 100644 reports/api/endpoints_test.go create mode 100644 reports/api/request.go create mode 100644 reports/api/response.go create mode 100644 reports/api/transport.go rename {re => reports}/generator.go (99%) create mode 100644 reports/handler.go create mode 100644 reports/middleware/authorization.go create mode 100644 reports/middleware/logging.go create mode 100644 reports/mocks/repository.go create mode 100644 reports/mocks/service.go create mode 100644 reports/postgres/init.go create mode 100644 reports/postgres/reports.go create mode 100644 reports/postgres/repository.go rename {re => reports}/reports.go (72%) create mode 100644 reports/service.go create mode 100644 reports/service_test.go create mode 100644 reports/status.go diff --git a/Makefile b/Makefile index 4904ac142..655d60cab 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MG_DOCKER_IMAGE_NAME_PREFIX ?= ghcr.io/absmach/magistrala BUILD_DIR = build -SERVICES = bootstrap provision re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms +SERVICES = bootstrap provision re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports DOCKERS = $(addprefix docker_,$(SERVICES)) DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES)) CGO_ENABLED ?= 0 diff --git a/apidocs/openapi/reports.yaml b/apidocs/openapi/reports.yaml new file mode 100644 index 000000000..d92a35593 --- /dev/null +++ b/apidocs/openapi/reports.yaml @@ -0,0 +1,556 @@ +# Copyright (c) Abstract Machines +# SPDX-License-Identifier: Apache-2.0 + +openapi: 3.0.1 +info: + title: Magistrala Reports Service API + description: | + HTTP API for managing reports service. + version: 0.15.1 +servers: + - url: http://localhost:9017 +tags: + - name: reports + description: Operations related to report configurations and generation +paths: + /{domainID}/reports: + post: + operationId: generateReport + summary: Generate a report + description: Generates a report based on the provided configuration or an existing config. The action determines the response format. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateReportRequest' + responses: + '200': + description: Report generated successfully (content varies by action) + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateReportResponse' + application/octet-stream: + schema: + type: string + format: binary + '400': + description: Invalid request parameters + '401': + description: Missing or invalid access token + '500': + $ref: '#/components/responses/ServiceError' + + /{domainID}/reports/configs: + post: + operationId: addReportConfig + summary: Create a report configuration + description: Creates a new report configuration. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddReportConfigRequest' + responses: + '201': + description: Report configuration created + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '400': + description: Invalid request body + '401': + description: Missing or invalid access token + '500': + $ref: '#/components/responses/ServiceError' + get: + operationId: listReportConfigs + summary: List report configurations + description: Retrieves a paginated list of report configurations. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/Offset' + - $ref: '#/components/parameters/Limit' + security: + - bearerAuth: [] + responses: + '200': + description: List of report configurations + content: + application/json: + schema: + $ref: '#/components/schemas/ListReportsConfigResponse' + '400': + description: Invalid query parameters + '401': + description: Missing or invalid access token + '500': + $ref: '#/components/responses/ServiceError' + + /{domainID}/reports/configs/{reportID}: + get: + operationId: viewReportConfig + summary: View a report configuration + description: Retrieves details of a specific report configuration. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + responses: + '200': + description: Report configuration details + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '404': + description: Report configuration not found + '401': + description: Missing or invalid access token + '500': + $ref: '#/components/responses/ServiceError' + patch: + operationId: updateReportConfig + summary: Update a report configuration + description: Updates specified fields of a report configuration. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateReportConfigRequest' + responses: + '200': + description: Report configuration updated + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '400': + description: Invalid request body + '401': + description: Missing or invalid access token + '404': + description: Report configuration not found + '500': + $ref: '#/components/responses/ServiceError' + delete: + operationId: deleteReportConfig + summary: Delete a report configuration + description: Permanently deletes a report configuration. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + responses: + '204': + description: Report configuration deleted + '401': + description: Missing or invalid access token + '404': + description: Report configuration not found + '500': + $ref: '#/components/responses/ServiceError' + + /{domainID}/reports/configs/{reportID}/schedule: + patch: + operationId: updateReportSchedule + summary: Update report schedule + description: Updates the schedule of a report configuration. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Schedule' + responses: + '200': + description: Schedule updated + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '400': + description: Invalid schedule + '401': + description: Missing or invalid access token + '404': + description: Report configuration not found + '500': + $ref: '#/components/responses/ServiceError' + + /{domainID}/reports/configs/{reportID}/enable: + post: + operationId: enableReportConfig + summary: Enable a report configuration + description: Enables a report configuration to generate scheduled reports. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + responses: + '200': + description: Report configuration enabled + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '401': + description: Missing or invalid access token + '404': + description: Report configuration not found + '500': + $ref: '#/components/responses/ServiceError' + + /{domainID}/reports/configs/{reportID}/disable: + post: + operationId: disableReportConfig + summary: Disable a report configuration + description: Disables a report configuration, stopping scheduled reports. + tags: + - reports + parameters: + - $ref: '#/components/parameters/DomainID' + - $ref: '#/components/parameters/ReportID' + security: + - bearerAuth: [] + responses: + '200': + description: Report configuration disabled + content: + application/json: + schema: + $ref: '#/components/schemas/ReportConfig' + '401': + description: Missing or invalid access token + '404': + description: Report configuration not found + '500': + $ref: '#/components/responses/ServiceError' + + /health: + get: + summary: Service health check + tags: + - health + responses: + '200': + $ref: '#/components/responses/HealthRes' + +components: + schemas: + ReportConfig: + type: object + properties: + id: + type: string + readOnly: true + name: + type: string + description: + type: string + domain_id: + type: string + readOnly: true + schedule: + $ref: '#/components/schemas/Schedule' + config: + $ref: '#/components/schemas/MetricConfig' + email: + $ref: '#/components/schemas/EmailSetting' + metrics: + type: array + items: + $ref: '#/components/schemas/ReqMetric' + status: + $ref: '#/components/schemas/Status' + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + updated_by: + type: string + readOnly: true + required: + - name + - metrics + - config + + Schedule: + type: object + properties: + recurring: + type: string + enum: [None, Daily, Weekly, Monthly] + recurring_period: + type: integer + minimum: 1 + start_time: + type: string + format: date-time + next_run: + type: string + format: date-time + readOnly: true + + MetricConfig: + type: object + properties: + title: + type: string + maxLength: 100 + format: + type: string + enum: [pdf, csv, html] + aggregation: + $ref: '#/components/schemas/AggConfig' + + AggConfig: + type: object + properties: + window: + type: string + function: + type: string + enum: [sum, average, max, min] + + EmailSetting: + type: object + properties: + recipients: + type: array + items: + type: string + format: email + subject: + type: string + body_template: + type: string + required: + - recipients + - subject + + ReqMetric: + type: object + properties: + name: + type: string + type: + type: string + enum: [gauge, counter, histogram] + parameters: + type: object + required: + - name + - type + + Status: + type: string + enum: [enabled, disabled] + + GenerateReportRequest: + type: object + properties: + action: + type: string + enum: [view, download, email] + config_id: + type: string + name: + type: string + description: + type: string + schedule: + $ref: '#/components/schemas/Schedule' + config: + $ref: '#/components/schemas/MetricConfig' + email: + $ref: '#/components/schemas/EmailSetting' + metrics: + type: array + items: + $ref: '#/components/schemas/ReqMetric' + required: + - action + + GenerateReportResponse: + type: object + properties: + total: + type: integer + from: + type: string + format: date-time + to: + type: string + format: date-time + aggregation: + $ref: '#/components/schemas/AggConfig' + reports: + type: array + items: + $ref: '#/components/schemas/Report' + + Report: + type: object + properties: + timestamp: + type: string + format: date-time + value: + type: number + metric_name: + type: string + + AddReportConfigRequest: + type: object + properties: + name: + type: string + description: + type: string + schedule: + $ref: '#/components/schemas/Schedule' + config: + $ref: '#/components/schemas/MetricConfig' + email: + $ref: '#/components/schemas/EmailSetting' + metrics: + type: array + items: + $ref: '#/components/schemas/ReqMetric' + status: + $ref: '#/components/schemas/Status' + required: + - name + - metrics + - config + + UpdateReportConfigRequest: + type: object + properties: + name: + type: string + description: + type: string + schedule: + $ref: '#/components/schemas/Schedule' + config: + $ref: '#/components/schemas/MetricConfig' + email: + $ref: '#/components/schemas/EmailSetting' + metrics: + type: array + items: + $ref: '#/components/schemas/ReqMetric' + status: + $ref: '#/components/schemas/Status' + + ListReportsConfigResponse: + type: object + properties: + total: + type: integer + offset: + type: integer + limit: + type: integer + report_configs: + type: array + items: + $ref: '#/components/schemas/ReportConfig' + + parameters: + DomainID: + name: domainID + in: path + required: true + schema: + type: string + ReportID: + name: reportID + in: path + required: true + schema: + type: string + Offset: + name: offset + in: query + schema: + type: integer + default: 0 + minimum: 0 + Limit: + name: limit + in: query + schema: + type: integer + default: 10 + minimum: 1 + maximum: 100 + + responses: + ServiceError: + description: Unexpected server error + HealthRes: + description: Service health status + content: + application/json: + schema: + type: object + properties: + status: + type: string + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/cmd/re/main.go b/cmd/re/main.go index c9fa19bb7..20b3bf213 100644 --- a/cmd/re/main.go +++ b/cmd/re/main.go @@ -18,9 +18,11 @@ import ( grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" "github.com/absmach/magistrala/consumers/writers/brokers" "github.com/absmach/magistrala/internal/email" + "github.com/absmach/magistrala/pkg/emailer" + pkglog "github.com/absmach/magistrala/pkg/logger" + "github.com/absmach/magistrala/pkg/ticker" "github.com/absmach/magistrala/re" httpapi "github.com/absmach/magistrala/re/api" - "github.com/absmach/magistrala/re/emailer" "github.com/absmach/magistrala/re/middleware" repg "github.com/absmach/magistrala/re/postgres" grpcClient "github.com/absmach/magistrala/readers/api/grpc" @@ -190,7 +192,7 @@ func main() { } defer authnClient.Close() logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure()) - runInfo := make(chan re.RunInfo, channBuffer) + runInfo := make(chan pkglog.RunInfo, channBuffer) domsGrpcCfg := grpcclient.Config{} if err := env.ParseWithOptions(&domsGrpcCfg, env.Options{Prefix: envPrefixDomains}); err != nil { @@ -285,7 +287,7 @@ func main() { } } -func newService(db pgclient.Database, runInfo chan re.RunInfo, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient) (re.Service, error) { +func newService(db pgclient.Database, runInfo chan pkglog.RunInfo, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient) (re.Service, error) { repo := repg.NewRepository(db) idp := uuid.New() @@ -294,7 +296,7 @@ func newService(db pgclient.Database, runInfo chan re.RunInfo, rePubSub messagin logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error())) } - csvc := re.NewService(repo, runInfo, idp, rePubSub, writersPub, alarmsPub, re.NewTicker(time.Second*30), emailerClient, readersClient) + csvc := re.NewService(repo, runInfo, idp, rePubSub, writersPub, alarmsPub, ticker.NewTicker(time.Second*30), emailerClient, readersClient) csvc, err = middleware.AuthorizationMiddleware(csvc, authz) if err != nil { return nil, err diff --git a/cmd/reports/main.go b/cmd/reports/main.go new file mode 100644 index 000000000..66bf5ffd2 --- /dev/null +++ b/cmd/reports/main.go @@ -0,0 +1,258 @@ +// 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" + grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" + "github.com/absmach/magistrala/internal/email" + "github.com/absmach/magistrala/pkg/emailer" + pkglog "github.com/absmach/magistrala/pkg/logger" + "github.com/absmach/magistrala/pkg/ticker" + grpcClient "github.com/absmach/magistrala/readers/api/grpc" + "github.com/absmach/magistrala/reports" + httpapi "github.com/absmach/magistrala/reports/api" + "github.com/absmach/magistrala/reports/middleware" + repg "github.com/absmach/magistrala/reports/postgres" + "github.com/absmach/supermq" + smqlog "github.com/absmach/supermq/logger" + authnsvc "github.com/absmach/supermq/pkg/authn/authsvc" + mgauthz "github.com/absmach/supermq/pkg/authz" + authzsvc "github.com/absmach/supermq/pkg/authz/authsvc" + domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient" + "github.com/absmach/supermq/pkg/grpcclient" + jaegerclient "github.com/absmach/supermq/pkg/jaeger" + pgclient "github.com/absmach/supermq/pkg/postgres" + "github.com/absmach/supermq/pkg/server" + httpserver "github.com/absmach/supermq/pkg/server/http" + "github.com/absmach/supermq/pkg/uuid" + "github.com/caarlos0/env/v11" + "github.com/go-chi/chi/v5" + "golang.org/x/sync/errgroup" +) + +const ( + svcName = "reports" + envPrefixDB = "MG_REPORTS_DB_" + envPrefixHTTP = "MG_REPORTS_HTTP_" + envPrefixAuth = "SMQ_AUTH_GRPC_" + defDB = "repo" + defSvcHTTPPort = "9017" + envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_" + envPrefixDomains = "SMQ_DOMAINS_GRPC_" +) + +// We use a buffered channel to prevent blocking, as logging is an expensive operation. +const channBuffer = 256 + +type config struct { + LogLevel string `env:"MG_REPORTS_LOG_LEVEL" envDefault:"info"` + InstanceID string `env:"MG_REPORTS_INSTANCE_ID" envDefault:""` + JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` + ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"` + TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"` + BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"` +} + +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 := smqlog.New(os.Stdout, cfg.LogLevel) + if err != nil { + log.Fatalf("failed to init logger: %s", err.Error()) + } + + var exitCode int + defer smqlog.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 + } + + 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, *repg.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 + } + + grpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + + return + } + authn, authnClient, err := authnsvc.NewAuthentication(ctx, grpcCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + + return + } + defer authnClient.Close() + logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure()) + + domsGrpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&domsGrpcCfg, env.Options{Prefix: envPrefixDomains}); err != nil { + logger.Error(fmt.Sprintf("failed to load domains gRPC client configuration : %s", err)) + exitCode = 1 + return + } + domAuthz, _, domainsHandler, err := domainsAuthz.NewAuthorization(ctx, domsGrpcCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer domainsHandler.Close() + + authz, authzClient, err := authzsvc.NewAuthorization(ctx, grpcCfg, domAuthz) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authzClient.Close() + logger.Info("AuthZ successfully connected to auth gRPC server " + authnClient.Secure()) + + 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(database, runInfo, authz, ec, logger, readersClient) + 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, authn, mux, logger, cfg.InstanceID), logger) + + if cfg.SendTelemetry { + chc := chclient.New(svcName, supermq.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(db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient) (reports.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())) + } + + csvc := reports.NewService(repo, runInfo, idp, ticker.NewTicker(time.Second*30), emailerClient, readersClient) + csvc, err = middleware.AuthorizationMiddleware(csvc, authz) + if err != nil { + return nil, err + } + csvc = middleware.LoggingMiddleware(csvc, logger) + + return csvc, nil +} diff --git a/docker/.env b/docker/.env index 96bd28b1b..8c76aba41 100644 --- a/docker/.env +++ b/docker/.env @@ -136,6 +136,24 @@ MG_ALARMS_DB_SSL_KEY= MG_ALARMS_DB_SSL_ROOT_CERT= MG_ALARMS_INSTANCE_ID= +### REPORTS +MG_REPORTS_LOG_LEVEL=debug +MG_REPORTS_HTTP_HOST=reports +MG_REPORTS_HTTP_PORT=9017 +MG_REPORTS_HTTP_SERVER_CERT= +MG_REPORTS_HTTP_SERVER_KEY= +MG_REPORTS_DB_HOST=reports-db +MG_REPORTS_DB_PORT=5432 +MG_REPORTS_DB_USER=magistrala +MG_REPORTS_DB_PASS=magistrala +MG_REPORTS_DB_NAME=reports +MG_REPORTS_DB_SSL_MODE=disable +MG_REPORTS_DB_SSL_CERT= +MG_REPORTS_DB_SSL_KEY= +MG_REPORTS_DB_SSL_ROOT_CERT= +MG_REPORTS_INSTANCE_ID= +MG_REPORTS_EMAIL_TEMPLATE=reports.tmpl + ### Certs SMQ_ADDONS_CERTS_PATH_PREFIX=./ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e2dbbf853..540781aa7 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -23,6 +23,7 @@ volumes: magistrala-re-db-volume: magistrala-auth-redis-volume: magistrala-alarms-db-volume: + magistrala-reports-db-volume: services: ui: @@ -353,3 +354,93 @@ services: target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + + reports-db: + image: postgres:16.2-alpine + container_name: magistrala-reports-db + restart: on-failure + command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}" + environment: + POSTGRES_USER: ${MG_REPORTS_DB_USER} + POSTGRES_PASSWORD: ${MG_REPORTS_DB_PASS} + POSTGRES_DB: ${MG_REPORTS_DB_NAME} + ports: + - 6020:5432 + networks: + - magistrala-base-net + volumes: + - magistrala-reports-db-volume:/var/lib/postgresql/data + + reports: + image: ghcr.io/absmach/magistrala/reports:${MG_RELEASE_TAG} + container_name: magistrala-reports + depends_on: + - reports-db + restart: on-failure + environment: + MG_REPORTS_LOG_LEVEL: ${MG_REPORTS_LOG_LEVEL} + MG_REPORTS_HTTP_PORT: ${MG_REPORTS_HTTP_PORT} + MG_REPORTS_HTTP_HOST: ${MG_REPORTS_HTTP_HOST} + MG_REPORTS_HTTP_SERVER_CERT: ${MG_REPORTS_HTTP_SERVER_CERT} + MG_REPORTS_HTTP_SERVER_KEY: ${MG_REPORTS_HTTP_SERVER_KEY} + MG_REPORTS_DB_HOST: ${MG_REPORTS_DB_HOST} + MG_REPORTS_DB_PORT: ${MG_REPORTS_DB_PORT} + MG_REPORTS_DB_USER: ${MG_REPORTS_DB_USER} + MG_REPORTS_DB_PASS: ${MG_REPORTS_DB_PASS} + MG_REPORTS_DB_NAME: ${MG_REPORTS_DB_NAME} + MG_REPORTS_DB_SSL_MODE: ${MG_REPORTS_DB_SSL_MODE} + MG_REPORTS_DB_SSL_CERT: ${MG_REPORTS_DB_SSL_CERT} + MG_REPORTS_DB_SSL_KEY: ${MG_REPORTS_DB_SSL_KEY} + MG_REPORTS_DB_SSL_ROOT_CERT: ${MG_REPORTS_DB_SSL_ROOT_CERT} + SMQ_MESSAGE_BROKER_URL: ${SMQ_MESSAGE_BROKER_URL} + SMQ_JAEGER_URL: ${SMQ_JAEGER_URL} + SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO} + SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY} + SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL} + SMQ_AUTH_GRPC_TIMEOUT: ${SMQ_AUTH_GRPC_TIMEOUT} + SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} + SMQ_AUTH_GRPC_CLIENT_KEY: ${SMQ_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} + SMQ_AUTH_GRPC_SERVER_CA_CERTS: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY} + SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST} + SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT} + MG_REPORTS_INSTANCE_ID: ${MG_RE_INSTANCE_ID} + 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_TEMPLATE: ${MG_EMAIL_TEMPLATE} + MG_TIMESCALE_READER_GRPC_URL: ${MG_TIMESCALE_READER_GRPC_URL} + MG_TIMESCALE_READER_GRPC_TIMEOUT: ${MG_TIMESCALE_READER_GRPC_TIMEOUT} + MG_TIMESCALE_READER_GRPC_CLIENT_CERT: ${MG_TIMESCALE_READER_GRPC_CLIENT_CERT} + MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS: ${MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS} + MG_TIMESCALE_READER_GRPC_CLIENT_KEY: ${MG_TIMESCALE_READER_GRPC_CLIENT_KEY} + SMQ_DOMAINS_GRPC_URL: ${SMQ_DOMAINS_GRPC_URL} + SMQ_DOMAINS_GRPC_TIMEOUT: ${SMQ_DOMAINS_GRPC_TIMEOUT} + SMQ_DOMAINS_GRPC_CLIENT_CERT: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt} + SMQ_DOMAINS_GRPC_CLIENT_KEY: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key} + SMQ_DOMAINS_GRPC_SERVER_CA_CERTS: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt} + ports: + - ${MG_REPORTS_HTTP_PORT}:${MG_REPORTS_HTTP_PORT} + networks: + - magistrala-base-net + volumes: + - ./templates/${MG_REPORTS_EMAIL_TEMPLATE}:/email.tmpl + # Auth gRPC client certificates + - type: bind + source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh index b56e3b35d..985662860 100755 --- a/docker/nginx/entrypoint.sh +++ b/docker/nginx/entrypoint.sh @@ -23,6 +23,7 @@ envsubst ' ${SMQ_NGINX_MQTTS_PORT} ${MG_RE_HTTP_PORT} ${MG_ALARMS_HTTP_PORT} + ${MG_REPORTS_HTTP_PORT} ${SMQ_WS_ADAPTER_HTTP_PORT}' /etc/nginx/nginx.conf exec nginx -g "daemon off;" diff --git a/docker/nginx/nginx-key.conf b/docker/nginx/nginx-key.conf index 7eced5788..abb5a7f34 100644 --- a/docker/nginx/nginx-key.conf +++ b/docker/nginx/nginx-key.conf @@ -100,13 +100,20 @@ http { proxy_pass http://re:${MG_RE_HTTP_PORT}; } - # Proxy pass to rule engine service + # Proxy pass to alarm service location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(alarms)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; proxy_pass http://alarms:${MG_ALARMS_HTTP_PORT}; } + # Proxy pass to reports service + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(reports)" { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://reports:${MG_REPORTS_HTTP_PORT}; + } + location /health { include snippets/proxy-headers.conf; proxy_pass http://clients:${SMQ_CLIENTS_HTTP_PORT}; diff --git a/docker/nginx/nginx-x509.conf b/docker/nginx/nginx-x509.conf index 4efdfb5bf..4ee2df72c 100644 --- a/docker/nginx/nginx-x509.conf +++ b/docker/nginx/nginx-x509.conf @@ -109,13 +109,20 @@ http { proxy_pass http://re:${MG_RE_HTTP_PORT}; } - # Proxy pass to rule engine service + # Proxy pass to alarms service location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(alarms)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; proxy_pass http://alarms:${MG_ALARMS_HTTP_PORT}; } + # Proxy pass to reports service + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(reports)" { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://reports:${MG_REPORTS_HTTP_PORT}; + } + location /health { include snippets/proxy-headers.conf; proxy_pass http://clients:${SMQ_CLIENTS_HTTP_PORT}; diff --git a/docker/templates/reports.tmpl b/docker/templates/reports.tmpl new file mode 100644 index 000000000..3dad54580 --- /dev/null +++ b/docker/templates/reports.tmpl @@ -0,0 +1,3 @@ +{{.Header}} +{{.Content}} +{{.Footer}} diff --git a/re/emailer/emailer.go b/pkg/emailer/emailer.go similarity index 61% rename from re/emailer/emailer.go rename to pkg/emailer/emailer.go index 314244111..bd85dd694 100644 --- a/re/emailer/emailer.go +++ b/pkg/emailer/emailer.go @@ -5,16 +5,20 @@ package emailer import ( "github.com/absmach/magistrala/internal/email" - "github.com/absmach/magistrala/re" ) -var _ re.Emailer = (*emailer)(nil) +var _ Emailer = (*emailer)(nil) + +type Emailer interface { + // SendEmailNotification sends an email to the recipients based on a trigger. + SendEmailNotification(to []string, from, subject, header, user, content, footer string, attachments map[string][]byte) error +} type emailer struct { agent *email.Agent } -func New(a *email.Config) (re.Emailer, error) { +func New(a *email.Config) (Emailer, error) { e, err := email.New(a) return &emailer{agent: e}, err } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 000000000..5daccfd24 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,12 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package logger + +import "log/slog" + +type RunInfo struct { + Level slog.Level + Details []slog.Attr + Message string +} diff --git a/re/schedule.go b/pkg/schedule/schedule.go similarity index 96% rename from re/schedule.go rename to pkg/schedule/schedule.go index dd6640f00..9ca6fde49 100644 --- a/re/schedule.go +++ b/pkg/schedule/schedule.go @@ -1,13 +1,16 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package re +package schedule import ( "encoding/json" + "errors" "time" ) +var ErrInvalidRecurringType = errors.New("invalid recurring type") + // Type can be daily, weekly or monthly. type Recurring uint diff --git a/re/ticker.go b/pkg/ticker/ticker.go similarity index 95% rename from re/ticker.go rename to pkg/ticker/ticker.go index b283ec878..7220bde07 100644 --- a/re/ticker.go +++ b/pkg/ticker/ticker.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package re +package ticker import "time" diff --git a/re/api/endpoints.go b/re/api/endpoints.go index a81b9a1f0..43fd44485 100644 --- a/re/api/endpoints.go +++ b/re/api/endpoints.go @@ -177,227 +177,3 @@ func disableRuleEndpoint(s re.Service) endpoint.Endpoint { return updateRuleStatusRes{Rule: rule}, err } } - -func generateReportEndpoint(svc re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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, re.ReportConfig{ - Name: req.Name, - DomainID: req.DomainID, - Config: req.Config, - Metrics: req.Metrics, - Email: req.Email, - }, req.action) - if err != nil { - return generateReportResp{}, err - } - - switch req.action { - case re.DownloadReport: - return downloadReportResp{ - File: res.File, - }, nil - case re.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - req := request.(updateReportScheduleReq) - if err := req.validate(); err != nil { - return updateReportConfigRes{}, err - } - - rpt := re.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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) - if err != nil { - return viewReportConfigRes{}, err - } - - return viewReportConfigRes{ReportConfig: cfg}, nil - } -} - -func addReportConfigEndpoint(svc re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 re.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - session, ok := ctx.Value(api.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 - } -} diff --git a/re/api/endpoints_test.go b/re/api/endpoints_test.go index 78f7cc8be..ff8379ae1 100644 --- a/re/api/endpoints_test.go +++ b/re/api/endpoints_test.go @@ -15,7 +15,7 @@ import ( "github.com/0x6flab/namegenerator" "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/errors" + pkgSch "github.com/absmach/magistrala/pkg/schedule" "github.com/absmach/magistrala/re" "github.com/absmach/magistrala/re/api" "github.com/absmach/magistrala/re/mocks" @@ -24,6 +24,7 @@ import ( smqlog "github.com/absmach/supermq/logger" smqauthn "github.com/absmach/supermq/pkg/authn" authnmocks "github.com/absmach/supermq/pkg/authn/mocks" + "github.com/absmach/supermq/pkg/errors" svcerr "github.com/absmach/supermq/pkg/errors/service" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" @@ -40,9 +41,9 @@ var ( validToken = "valid" invalidToken = "invalid" now = time.Now().UTC().Truncate(time.Minute) - schedule = re.Schedule{ + schedule = pkgSch.Schedule{ StartDateTime: now.Add(-1 * time.Hour), - Recurring: re.Daily, + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, } @@ -55,31 +56,6 @@ var ( "name": "test", }, } - reportConfig = re.ReportConfig{ - ID: validID, - Name: namegen.Generate(), - DomainID: domainID, - Schedule: schedule, - Status: re.EnabledStatus, - Metrics: []re.ReqMetric{ - { - ChannelID: "channel1", - ClientIDs: []string{"client1"}, - Name: "metric_name", - }, - }, - Config: &re.MetricConfig{ - From: "now()-1h", - To: "now()", - Title: title, - Aggregation: re.AggConfig{AggType: re.AggregationAVG, Interval: "1h"}, - }, - Email: &re.EmailSetting{ - To: []string{"test@example.com"}, - Subject: "Test Report", - }, - } - title = "test_title" ) type testRequest struct { @@ -991,686 +967,3 @@ type respBody struct { ID string `json:"id"` Status re.Status `json:"status"` } - -func TestAddReportConfigEndpoint(t *testing.T) { - ts, svc, authn := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - cfg re.ReportConfig - domainID string - token string - contentType string - status int - authnRes smqauthn.Session - authnErr error - svcRes re.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 service error", - token: validToken, - domainID: domainID, - authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, - cfg: reportConfig, - contentType: contentType, - svcErr: svcerr.ErrAuthorization, - status: http.StatusForbidden, - err: svcerr.ErrAuthorization, - }, - } - - 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 := 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.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).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 := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - query string - domainID string - token string - session smqauthn.Session - listReportsResponse re.ReportConfigPage - status int - authnErr error - err error - }{ - { - desc: "list reports config successfully", - domainID: domainID, - token: validToken, - status: http.StatusOK, - listReportsResponse: re.ReportConfigPage{ - ReportConfigs: []re.ReportConfig{reportConfig}, - PageMeta: re.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 := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - updateReq re.ReportConfig - contentType string - session smqauthn.Session - svcResp re.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: re.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 := 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 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 := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp re.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: re.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 := newRuleEngineServer() - defer ts.Close() - - cases := []struct { - desc string - token string - id string - domainID string - session smqauthn.Session - svcResp re.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: re.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() - }) - } -} diff --git a/re/api/requests.go b/re/api/requests.go index 7d204bbd1..0de0729c4 100644 --- a/re/api/requests.go +++ b/re/api/requests.go @@ -4,30 +4,16 @@ package api import ( - "fmt" - + "github.com/absmach/magistrala/pkg/schedule" "github.com/absmach/magistrala/re" api "github.com/absmach/supermq/api/http" apiutil "github.com/absmach/supermq/api/http/util" - "github.com/absmach/supermq/pkg/errors" - svcerr "github.com/absmach/supermq/pkg/errors/service" -) - -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") - errTitleSize = errors.New("invalid title size") ) const ( maxLimitSize = 1000 MaxNameSize = 1024 MaxTitleSize = 37 - - errInvalidMetric = "invalid metric[%d]: %w" ) type addRuleReq struct { @@ -85,7 +71,7 @@ func (req updateRuleReq) validate() error { type updateRuleScheduleReq struct { id string - Schedule re.Schedule `json:"schedule,omitempty"` + Schedule schedule.Schedule `json:"schedule,omitempty"` } func (req updateRuleScheduleReq) validate() error { @@ -119,143 +105,3 @@ func (req deleteRuleReq) validate() error { return nil } - -type updateReportConfigReq struct { - re.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 re.Schedule `json:"schedule,omitempty"` -} - -func (req updateReportScheduleReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type addReportConfigReq struct { - re.ReportConfig `json:",inline"` -} - -func (req addReportConfigReq) validate() error { - if req.Name == "" { - return apiutil.ErrMissingName - } - return validateReportConfig(req.ReportConfig, false, false) -} - -type viewReportConfigReq struct { - ID string `json:"id"` -} - -func (req viewReportConfigReq) validate() error { - if req.ID == "" { - return apiutil.ErrMissingID - } - return nil -} - -type listReportsConfigReq struct { - re.PageMeta `json:",inline"` -} - -func (req listReportsConfigReq) validate() error { - if req.Limit > maxLimitSize { - return svcerr.ErrMalformedEntity - } - 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 { - re.ReportConfig - action re.ReportAction -} - -func (req generateReportReq) validate() error { - if len(req.Config.Title) > MaxTitleSize { - return errors.Wrap(apiutil.ErrValidation, errTitleSize) - } - - switch req.action { - case re.ViewReport, re.DownloadReport: - return validateReportConfig(req.ReportConfig, true, true) - case re.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 re.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 errMissingReportConfig - } - if err := req.Config.Validate(); err != nil { - return errors.Wrap(apiutil.ErrValidation, err) - } - - if skipEmailValidation { - return nil - } - if req.Email == nil { - return errMissingReportEmailConfig - } - if err := req.Email.Validate(); err != nil { - return errors.Wrap(apiutil.ErrValidation, err) - } - - if skipSchedularValidation { - return nil - } - - return validateScheduler(req.Schedule) -} - -func validateScheduler(sch re.Schedule) error { - if sch.Recurring != re.None && sch.RecurringPeriod < 1 { - return errInvalidRecurringPeriod - } - return nil -} diff --git a/re/api/responses.go b/re/api/responses.go index 31fac1adc..d13753dc1 100644 --- a/re/api/responses.go +++ b/re/api/responses.go @@ -6,7 +6,6 @@ package api import ( "fmt" "net/http" - "time" "github.com/absmach/magistrala/re" "github.com/absmach/supermq" @@ -19,11 +18,6 @@ var ( _ supermq.Response = (*rulesPageRes)(nil) _ supermq.Response = (*updateRuleRes)(nil) _ supermq.Response = (*deleteRuleRes)(nil) - _ supermq.Response = (*addReportConfigRes)(nil) - _ supermq.Response = (*viewReportConfigRes)(nil) - _ supermq.Response = (*updateReportConfigRes)(nil) - _ supermq.Response = (*deleteReportConfigRes)(nil) - _ supermq.Response = (*listReportsConfigRes)(nil) ) type pageRes struct { @@ -142,144 +136,3 @@ func (res deleteRuleRes) Headers() map[string]string { func (res deleteRuleRes) Empty() bool { return true } - -type generateReportResp struct { - Total uint64 `json:"total"` - From time.Time `json:"from,omitempty"` - To time.Time `json:"to,omitempty"` - Aggregation re.AggConfig `json:"aggregation,omitempty"` - Reports []re.Report `json:"reports,omitempty"` -} - -func (res generateReportResp) Code() int { - return http.StatusCreated -} - -func (res generateReportResp) Headers() map[string]string { - return map[string]string{} -} - -func (res generateReportResp) Empty() bool { - return false -} - -type addReportConfigRes struct { - re.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 { - re.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 { - re.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 []re.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 re.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 -} diff --git a/re/api/transport.go b/re/api/transport.go index 057982cfe..331db99fe 100644 --- a/re/api/transport.go +++ b/re/api/transport.go @@ -6,7 +6,6 @@ package api import ( "context" "encoding/json" - "fmt" "log/slog" "net/http" "strings" @@ -100,72 +99,6 @@ func MakeHandler(svc re.Service, authn mgauthn.Authentication, mux *chi.Mux, log ), "disable_rule").ServeHTTP) }) }) - 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("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( - viewReportConfigEndpoint(svc), - decodeViewReportConfigRequest, - api.EncodeResponse, - opts..., - ), "view_report_config").ServeHTTP) - - r.Patch("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( - updateReportConfigEndpoint(svc), - decodeUpdateReportConfigRequest, - api.EncodeResponse, - opts..., - ), "update_report_config").ServeHTTP) - - r.Patch("/{reportID}/schedule", otelhttp.NewHandler(kithttp.NewServer( - updateReportScheduleEndpoint(svc), - decodeUpdateReportScheduleRequest, - api.EncodeResponse, - opts..., - ), "update_report_scheduler").ServeHTTP) - - r.Delete("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( - deleteReportConfigEndpoint(svc), - decodeDeleteReportConfigRequest, - api.EncodeResponse, - opts..., - ), "delete_report_config").ServeHTTP) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - listReportsConfigEndpoint(svc), - decodeListReportsConfigRequest, - api.EncodeResponse, - opts..., - ), "list_reports_config").ServeHTTP) - - r.Post("/{reportID}/enable", otelhttp.NewHandler(kithttp.NewServer( - enableReportConfigEndpoint(svc), - decodeUpdateReportStatusRequest, - api.EncodeResponse, - opts..., - ), "enable_report_config").ServeHTTP) - - r.Post("/{reportID}/disable", otelhttp.NewHandler(kithttp.NewServer( - disableReportConfigEndpoint(svc), - decodeUpdateReportStatusRequest, - api.EncodeResponse, - opts..., - ), "disable_report_config").ServeHTTP) - }) - }) }) }) @@ -276,136 +209,3 @@ func decodeDeleteRuleRequest(_ context.Context, r *http.Request) (interface{}, e return deleteRuleReq{id: id}, nil } - -func decodeGenerateReportRequest(_ context.Context, r *http.Request) (interface{}, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) - } - - a, err := apiutil.ReadStringQuery(r, actionKey, defAction) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - action, err := re.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) (interface{}, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) - } - var config re.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) (interface{}, error) { - id := chi.URLParam(r, reportIdKey) - return viewReportConfigReq{ID: id}, nil -} - -func decodeUpdateReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) - } - var config re.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) (interface{}, error) { - if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { - return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) - } - - req := updateReportScheduleReq{ - id: chi.URLParam(r, reportIdKey), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) - } - - return req, nil -} - -func decodeUpdateReportStatusRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := updateReportStatusReq{ - id: chi.URLParam(r, reportIdKey), - } - return req, nil -} - -func decodeDeleteReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) { - id := chi.URLParam(r, reportIdKey) - return deleteReportConfigReq{ID: id}, nil -} - -func decodeListReportsConfigRequest(_ context.Context, r *http.Request) (interface{}, 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 := re.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) - } - return listReportsConfigReq{ - PageMeta: re.PageMeta{ - Offset: offset, - Limit: limit, - Status: st, - Name: name, - }, - }, nil -} - -func encodeFileDownloadResponse(_ context.Context, w http.ResponseWriter, response interface{}) 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.(supermq.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/re/emailer.go b/re/emailer.go deleted file mode 100644 index 67da5c8ac..000000000 --- a/re/emailer.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package re - -type Emailer interface { - // SendEmailNotification sends an email to the recipients based on a trigger. - SendEmailNotification(to []string, from, subject, header, user, content, footer string, attachments map[string][]byte) error -} diff --git a/re/emailer/doc.go b/re/emailer/doc.go deleted file mode 100644 index 0f6aac17f..000000000 --- a/re/emailer/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Package emailer contains the domain concept definitions needed to support -// Magistrala re email service functionality. -package emailer diff --git a/re/handlers.go b/re/handlers.go index 00eff1fa8..d7184ba99 100644 --- a/re/handlers.go +++ b/re/handlers.go @@ -11,6 +11,7 @@ import ( "strings" "time" + pkglog "github.com/absmach/magistrala/pkg/logger" "github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/pkg/messaging" lua "github.com/yuin/gopher-lua" @@ -77,7 +78,7 @@ func matchSubject(published, subscribed string) bool { return len(s) == n } -func (re *re) process(ctx context.Context, r Rule, msg *messaging.Message) RunInfo { +func (re *re) process(ctx context.Context, r Rule, msg *messaging.Message) pkglog.RunInfo { l := lua.NewState() defer l.Close() preload(l) @@ -99,30 +100,30 @@ func (re *re) process(ctx context.Context, r Rule, msg *messaging.Message) RunIn slog.Time("exec_time", time.Now().UTC()), } if err := l.DoString(r.Logic.Value); err != nil { - return RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to run rule logic: %s", err), Details: details} + 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 RunInfo{Level: slog.LevelWarn, Message: "rule with nil script result", Details: details} + 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.Logic.Outputs) == 0 { - return RunInfo{Level: slog.LevelWarn, Message: "rule with no output channels", Details: details} + return pkglog.RunInfo{Level: slog.LevelWarn, Message: "rule with no output channels", Details: details} } var err error res := convertLua(result) for _, o := range r.Logic.Outputs { // If value is false, don't run the follow-up. if v, ok := res.(bool); ok && !v { - return RunInfo{Level: slog.LevelInfo, Message: "logic returned false", Details: details} + 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 := RunInfo{Level: slog.LevelInfo, Message: "rule processed successfully", Details: details} + 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) @@ -161,7 +162,7 @@ func (re *re) StartScheduler(ctx context.Context) error { page, err := re.repo.ListRules(ctx, pm) if err != nil { - re.runInfo <- RunInfo{ + re.runInfo <- pkglog.RunInfo{ Level: slog.LevelError, Message: fmt.Sprintf("failed to list rules: %s", err), Details: []slog.Attr{slog.Time("due", due)}, @@ -173,7 +174,7 @@ func (re *re) StartScheduler(ctx context.Context) error { for _, r := range page.Rules { go func(rule Rule) { if _, err := re.repo.UpdateRuleDue(ctx, rule.ID, rule.Schedule.NextDue()); err != nil { - re.runInfo <- RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to update rule: %s", err), Details: []slog.Attr{slog.Time("time", time.Now().UTC())}} + 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 } @@ -188,39 +189,6 @@ func (re *re) StartScheduler(ctx context.Context) error { } // Reset due, it will reset in the page meta as well. due = time.Now().UTC() - - reportConfigs, err := re.repo.ListReportsConfig(ctx, pm) - if err != nil { - re.runInfo <- 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 := re.repo.UpdateReportDue(ctx, cfg.ID, cfg.Schedule.NextDue()); err != nil { - re.runInfo <- RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to update report: %s", err), Details: []slog.Attr{slog.Time("time", time.Now().UTC())}} - return - } - _, err := re.generateReport(ctx, cfg, EmailReport) - ret := 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) - } - re.runInfo <- ret - }(c) - } } } } diff --git a/re/middleware/authorization.go b/re/middleware/authorization.go index 68fe52dee..edb9b73e8 100644 --- a/re/middleware/authorization.go +++ b/re/middleware/authorization.go @@ -15,15 +15,10 @@ import ( ) 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") - 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") - errDomainGenerateReports = errors.New("not authorized to generate reports in domain") + 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 { @@ -167,150 +162,6 @@ func (am *authorizationMiddleware) DisableRule(ctx context.Context, session auth return am.svc.DisableRule(ctx, session, id) } -func (am *authorizationMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg re.ReportConfig) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainCreateConfigs, err) - } - - return am.svc.AddReportConfig(ctx, session, cfg) -} - -func (am *authorizationMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainViewConfigs, err) - } - - return am.svc.ViewReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg re.ReportConfig) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.UpdateReportConfig(ctx, session, cfg) -} - -func (am *authorizationMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg re.ReportConfig) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainDeleteConfigs, 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, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); 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 re.PageMeta) (re.ReportConfigPage, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfigPage{}, errors.Wrap(errDomainViewConfigs, err) - } - - return am.svc.ListReportsConfig(ctx, session, pm) -} - -func (am *authorizationMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.EnableReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err) - } - - return am.svc.DisableReportConfig(ctx, session, id) -} - -func (am *authorizationMiddleware) GenerateReport(ctx context.Context, session authn.Session, config re.ReportConfig, action re.ReportAction) (re.ReportPage, error) { - if err := am.authorize(ctx, smqauthz.PolicyReq{ - Domain: session.DomainID, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Subject: session.DomainUserID, - Object: session.DomainID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return re.ReportPage{}, errors.Wrap(errDomainGenerateReports, err) - } - - return am.svc.GenerateReport(ctx, session, config, action) -} - func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error { return am.svc.StartScheduler(ctx) } diff --git a/re/middleware/logging.go b/re/middleware/logging.go index 488d363eb..90210e375 100644 --- a/re/middleware/logging.go +++ b/re/middleware/logging.go @@ -220,174 +220,3 @@ func (lm *loggingMiddleware) Handle(msg *messaging.Message) (err error) { func (lm *loggingMiddleware) Cancel() error { return lm.Cancel() } - -func (lm *loggingMiddleware) GenerateReport(ctx context.Context, session authn.Session, config re.ReportConfig, action re.ReportAction) (page re.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 re.ReportConfig) (res re.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) (res re.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) -} - -func (lm *loggingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, config re.ReportConfig) (res re.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 re.ReportConfig) (res re.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 re.PageMeta) (pg re.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 re.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 re.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) -} diff --git a/re/mocks/repository.go b/re/mocks/repository.go index a59eff37e..f549cd19d 100644 --- a/re/mocks/repository.go +++ b/re/mocks/repository.go @@ -42,61 +42,6 @@ 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 re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for AddReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.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 -// - cfg -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 re.ReportConfig)) *Repository_AddReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(re.ReportConfig)) - }) - return _c -} - -func (_c *Repository_AddReportConfig_Call) Return(reportConfig re.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 re.ReportConfig) (re.ReportConfig, error)) *Repository_AddReportConfig_Call { - _c.Call.Return(run) - return _c -} - // 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) @@ -152,61 +97,6 @@ func (_c *Repository_AddRule_Call) RunAndReturn(run func(ctx context.Context, r return _c } -// ListReportsConfig provides a mock function for the type Repository -func (_mock *Repository) ListReportsConfig(ctx context.Context, pm re.PageMeta) (re.ReportConfigPage, error) { - ret := _mock.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for ListReportsConfig") - } - - var r0 re.ReportConfigPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.PageMeta) (re.ReportConfigPage, error)); ok { - return returnFunc(ctx, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.PageMeta) re.ReportConfigPage); ok { - r0 = returnFunc(ctx, pm) - } else { - r0 = ret.Get(0).(re.ReportConfigPage) - } - 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_ListReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReportsConfig' -type Repository_ListReportsConfig_Call struct { - *mock.Call -} - -// ListReportsConfig is a helper method to define mock.On call -// - ctx -// - pm -func (_e *Repository_Expecter) ListReportsConfig(ctx interface{}, pm interface{}) *Repository_ListReportsConfig_Call { - return &Repository_ListReportsConfig_Call{Call: _e.mock.On("ListReportsConfig", ctx, pm)} -} - -func (_c *Repository_ListReportsConfig_Call) Run(run func(ctx context.Context, pm re.PageMeta)) *Repository_ListReportsConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(re.PageMeta)) - }) - return _c -} - -func (_c *Repository_ListReportsConfig_Call) Return(reportConfigPage re.ReportConfigPage, err error) *Repository_ListReportsConfig_Call { - _c.Call.Return(reportConfigPage, err) - return _c -} - -func (_c *Repository_ListReportsConfig_Call) RunAndReturn(run func(ctx context.Context, pm re.PageMeta) (re.ReportConfigPage, error)) *Repository_ListReportsConfig_Call { - _c.Call.Return(run) - return _c -} - // ListRules provides a mock function for the type Repository func (_mock *Repository) ListRules(ctx context.Context, pm re.PageMeta) (re.Page, error) { ret := _mock.Called(ctx, pm) @@ -262,52 +152,6 @@ func (_c *Repository_ListRules_Call) RunAndReturn(run func(ctx context.Context, 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 -// - id -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) { - run(args[0].(context.Context), args[1].(string)) - }) - 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 -} - // RemoveRule provides a mock function for the type Repository func (_mock *Repository) RemoveRule(ctx context.Context, id string) error { ret := _mock.Called(ctx, id) @@ -354,227 +198,6 @@ func (_c *Repository_RemoveRule_Call) RunAndReturn(run func(ctx context.Context, return _c } -// UpdateReportConfig provides a mock function for the type Repository -func (_mock *Repository) UpdateReportConfig(ctx context.Context, cfg re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.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 -// - cfg -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 re.ReportConfig)) *Repository_UpdateReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(re.ReportConfig)) - }) - return _c -} - -func (_c *Repository_UpdateReportConfig_Call) Return(reportConfig re.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 re.ReportConfig) (re.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 re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfigStatus") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.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 -// - cfg -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 re.ReportConfig)) *Repository_UpdateReportConfigStatus_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(re.ReportConfig)) - }) - return _c -} - -func (_c *Repository_UpdateReportConfigStatus_Call) Return(reportConfig re.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 re.ReportConfig) (re.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) (re.ReportConfig, error) { - ret := _mock.Called(ctx, id, due) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportDue") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) (re.ReportConfig, error)); ok { - return returnFunc(ctx, id, due) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) re.ReportConfig); ok { - r0 = returnFunc(ctx, id, due) - } else { - r0 = ret.Get(0).(re.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 -// - id -// - due -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) { - run(args[0].(context.Context), args[1].(string), args[2].(time.Time)) - }) - return _c -} - -func (_c *Repository_UpdateReportDue_Call) Return(reportConfig re.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) (re.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 re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportSchedule") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, re.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 -// - cfg -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 re.ReportConfig)) *Repository_UpdateReportSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(re.ReportConfig)) - }) - return _c -} - -func (_c *Repository_UpdateReportSchedule_Call) Return(reportConfig re.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 re.ReportConfig) (re.ReportConfig, error)) *Repository_UpdateReportSchedule_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) @@ -796,61 +419,6 @@ func (_c *Repository_UpdateRuleStatus_Call) RunAndReturn(run func(ctx context.Co return _c } -// ViewReportConfig provides a mock function for the type Repository -func (_mock *Repository) ViewReportConfig(ctx context.Context, id string) (re.ReportConfig, error) { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for ViewReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (re.ReportConfig, error)); ok { - return returnFunc(ctx, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) re.ReportConfig); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Get(0).(re.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 -// - id -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) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *Repository_ViewReportConfig_Call) Return(reportConfig re.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) (re.ReportConfig, error)) *Repository_ViewReportConfig_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) diff --git a/re/mocks/service.go b/re/mocks/service.go index 0c7b9adc4..a54a53179 100644 --- a/re/mocks/service.go +++ b/re/mocks/service.go @@ -43,62 +43,6 @@ 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 re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for AddReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.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 -// - session -// - cfg -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 re.ReportConfig)) *Service_AddReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.ReportConfig)) - }) - return _c -} - -func (_c *Service_AddReportConfig_Call) Return(reportConfig re.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 re.ReportConfig) (re.ReportConfig, error)) *Service_AddReportConfig_Call { - _c.Call.Return(run) - return _c -} - // 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) @@ -199,62 +143,6 @@ func (_c *Service_Cancel_Call) RunAndReturn(run func() error) *Service_Cancel_Ca return _c } -// DisableReportConfig provides a mock function for the type Service -func (_mock *Service) DisableReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DisableReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(re.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 -// - session -// - id -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) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) - }) - return _c -} - -func (_c *Service_DisableReportConfig_Call) Return(reportConfig re.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) (re.ReportConfig, error)) *Service_DisableReportConfig_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) @@ -311,62 +199,6 @@ func (_c *Service_DisableRule_Call) RunAndReturn(run func(ctx context.Context, s return _c } -// EnableReportConfig provides a mock function for the type Service -func (_mock *Service) EnableReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for EnableReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(re.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 -// - session -// - id -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) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) - }) - return _c -} - -func (_c *Service_EnableReportConfig_Call) Return(reportConfig re.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) (re.ReportConfig, error)) *Service_EnableReportConfig_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) @@ -423,63 +255,6 @@ func (_c *Service_EnableRule_Call) RunAndReturn(run func(ctx context.Context, se return _c } -// GenerateReport provides a mock function for the type Service -func (_mock *Service) GenerateReport(ctx context.Context, session authn.Session, config re.ReportConfig, action re.ReportAction) (re.ReportPage, error) { - ret := _mock.Called(ctx, session, config, action) - - if len(ret) == 0 { - panic("no return value specified for GenerateReport") - } - - var r0 re.ReportPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig, re.ReportAction) (re.ReportPage, error)); ok { - return returnFunc(ctx, session, config, action) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig, re.ReportAction) re.ReportPage); ok { - r0 = returnFunc(ctx, session, config, action) - } else { - r0 = ret.Get(0).(re.ReportPage) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.ReportConfig, re.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 -// - session -// - config -// - action -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 re.ReportConfig, action re.ReportAction)) *Service_GenerateReport_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.ReportConfig), args[3].(re.ReportAction)) - }) - return _c -} - -func (_c *Service_GenerateReport_Call) Return(reportPage re.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 re.ReportConfig, action re.ReportAction) (re.ReportPage, error)) *Service_GenerateReport_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) @@ -525,62 +300,6 @@ func (_c *Service_Handle_Call) RunAndReturn(run func(msg *messaging.Message) err return _c } -// ListReportsConfig provides a mock function for the type Service -func (_mock *Service) ListReportsConfig(ctx context.Context, session authn.Session, pm re.PageMeta) (re.ReportConfigPage, error) { - ret := _mock.Called(ctx, session, pm) - - if len(ret) == 0 { - panic("no return value specified for ListReportsConfig") - } - - var r0 re.ReportConfigPage - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.PageMeta) (re.ReportConfigPage, error)); ok { - return returnFunc(ctx, session, pm) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.PageMeta) re.ReportConfigPage); ok { - r0 = returnFunc(ctx, session, pm) - } else { - r0 = ret.Get(0).(re.ReportConfigPage) - } - 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_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 -// - session -// - pm -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 re.PageMeta)) *Service_ListReportsConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.PageMeta)) - }) - return _c -} - -func (_c *Service_ListReportsConfig_Call) Return(reportConfigPage re.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 re.PageMeta) (re.ReportConfigPage, error)) *Service_ListReportsConfig_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) @@ -637,53 +356,6 @@ func (_c *Service_ListRules_Call) RunAndReturn(run func(ctx context.Context, ses 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 -// - session -// - id -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) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) - }) - 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 -} - // 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) @@ -776,118 +448,6 @@ func (_c *Service_StartScheduler_Call) RunAndReturn(run func(ctx context.Context return _c } -// UpdateReportConfig provides a mock function for the type Service -func (_mock *Service) UpdateReportConfig(ctx context.Context, session authn.Session, cfg re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.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 -// - session -// - cfg -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 re.ReportConfig)) *Service_UpdateReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.ReportConfig)) - }) - return _c -} - -func (_c *Service_UpdateReportConfig_Call) Return(reportConfig re.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 re.ReportConfig) (re.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 re.ReportConfig) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, cfg) - - if len(ret) == 0 { - panic("no return value specified for UpdateReportSchedule") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, cfg) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.ReportConfig) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, cfg) - } else { - r0 = ret.Get(0).(re.ReportConfig) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.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 -// - session -// - cfg -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 re.ReportConfig)) *Service_UpdateReportSchedule_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.ReportConfig)) - }) - return _c -} - -func (_c *Service_UpdateReportSchedule_Call) Return(reportConfig re.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 re.ReportConfig) (re.ReportConfig, error)) *Service_UpdateReportSchedule_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) @@ -1000,62 +560,6 @@ func (_c *Service_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.Con return _c } -// ViewReportConfig provides a mock function for the type Service -func (_mock *Service) ViewReportConfig(ctx context.Context, session authn.Session, id string) (re.ReportConfig, error) { - ret := _mock.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewReportConfig") - } - - var r0 re.ReportConfig - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (re.ReportConfig, error)); ok { - return returnFunc(ctx, session, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) re.ReportConfig); ok { - r0 = returnFunc(ctx, session, id) - } else { - r0 = ret.Get(0).(re.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_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 -// - session -// - id -func (_e *Service_Expecter) ViewReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_ViewReportConfig_Call { - return &Service_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, session, id)} -} - -func (_c *Service_ViewReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewReportConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) - }) - return _c -} - -func (_c *Service_ViewReportConfig_Call) Return(reportConfig re.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) (re.ReportConfig, error)) *Service_ViewReportConfig_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) (re.Rule, error) { ret := _mock.Called(ctx, session, id) diff --git a/re/postgres/init.go b/re/postgres/init.go index a3e7d012d..843f80d19 100644 --- a/re/postgres/init.go +++ b/re/postgres/init.go @@ -43,32 +43,6 @@ func Migration() *migrate.MemoryMigrationSource { `DROP TABLE IF EXISTS rules`, }, }, - { - Id: "rules_02", - 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), - time TIMESTAMP, - recurring SMALLINT, - recurring_period SMALLINT, - start_datetime TIMESTAMP, - config JSONB, - email JSONB, - metrics JSONB - );`, - }, - Down: []string{ - `DROP TABLE IF EXISTS report_config;`, - }, - }, }, } } diff --git a/re/postgres/repository.go b/re/postgres/repository.go index 081431613..c2fd453ec 100644 --- a/re/postgres/repository.go +++ b/re/postgres/repository.go @@ -353,318 +353,3 @@ func pageRulesQuery(pm re.PageMeta) string { return q } - -func (repo *PostgresRepository) AddReportConfig(ctx context.Context, cfg re.ReportConfig) (re.ReportConfig, error) { - q := ` - INSERT INTO report_config (id, name, description, domain_id, config, metrics, - email, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status) - VALUES (:id, :name, :description, :domain_id, :config, :metrics, - :email, :start_datetime, :time, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status) - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - dbr, err := reportToDb(cfg) - if err != nil { - return re.ReportConfig{}, err - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return re.ReportConfig{}, err - } - defer row.Close() - - var dbReport dbReport - if row.Next() { - if err := row.StructScan(&dbReport); err != nil { - return re.ReportConfig{}, err - } - } - - report, err := dbToReport(dbReport) - if err != nil { - return re.ReportConfig{}, err - } - - return report, nil -} - -func (repo *PostgresRepository) ViewReportConfig(ctx context.Context, id string) (re.ReportConfig, error) { - q := ` - SELECT id, name, description, domain_id, config, metrics, - email, start_datetime, time, 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 re.ReportConfig{}, err - } - var dbr dbReport - if err := row.StructScan(&dbr); err != nil { - return re.ReportConfig{}, err - } - rpt, err := dbToReport(dbr) - if err != nil { - return re.ReportConfig{}, err - } - - return rpt, nil -} - -func (repo *PostgresRepository) UpdateReportConfigStatus(ctx context.Context, cfg re.ReportConfig) (re.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, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;` - - dbRpt, err := reportToDb(cfg) - if err != nil { - return re.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - row, err := repo.DB.NamedQueryContext(ctx, q, dbRpt) - if err != nil { - return re.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - dbr := dbReport{} - if row.Next() { - if err := row.StructScan(&dbr); err != nil { - return re.ReportConfig{}, err - } - - res, err := dbToReport(dbr) - if err != nil { - return re.ReportConfig{}, err - } - return res, err - } - - return re.ReportConfig{}, repoerr.ErrNotFound -} - -func (repo *PostgresRepository) UpdateReportConfig(ctx context.Context, cfg re.ReportConfig) (re.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 = fmt.Sprintf("%s", 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, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - `, q) - - dbr, err := reportToDb(cfg) - if err != nil { - return re.ReportConfig{}, err - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return re.ReportConfig{}, err - } - defer row.Close() - - var dbReport dbReport - if row.Next() { - if err := row.StructScan(&dbReport); err != nil { - return re.ReportConfig{}, err - } - } - rpt, err := dbToReport(dbReport) - if err != nil { - return re.ReportConfig{}, err - } - - return rpt, nil -} - -func (repo *PostgresRepository) UpdateReportSchedule(ctx context.Context, cfg re.ReportConfig) (re.ReportConfig, error) { - q := ` - UPDATE report_config - 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, description, domain_id, config, metrics, - email, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - - dbr, err := reportToDb(cfg) - if err != nil { - return re.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - row, err := repo.DB.NamedQueryContext(ctx, q, dbr) - if err != nil { - return re.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - var dbReport dbReport - if row.Next() { - if err := row.StructScan(&dbReport); err != nil { - return re.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - } - report, err := dbToReport(dbReport) - if err != nil { - return re.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) ListReportsConfig(ctx context.Context, pm re.PageMeta) (re.ReportConfigPage, error) { - listReportsQuery := ` - SELECT id, name, description, domain_id, metrics, email, config, - start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status - FROM report_config rc %s %s; - ` - - pgData := "" - if pm.Limit != 0 { - pgData = "LIMIT :limit" - } - if pm.Offset != 0 { - pgData += " OFFSET :offset" - } - pq := pageReportQuery(pm) - q := fmt.Sprintf(listReportsQuery, pq, pgData) - rows, err := repo.DB.NamedQueryContext(ctx, q, pm) - if err != nil { - return re.ReportConfigPage{}, err - } - defer rows.Close() - - cfgs := []re.ReportConfig{} - for rows.Next() { - var r dbReport - if err := rows.StructScan(&r); err != nil { - return re.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - rpt, err := dbToReport(r) - if err != nil { - return re.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 re.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err) - } - pm.Total = total - ret := re.ReportConfigPage{ - PageMeta: pm, - ReportConfigs: cfgs, - } - - return ret, nil -} - -func (repo *PostgresRepository) UpdateReportDue(ctx context.Context, id string, due time.Time) (re.ReportConfig, error) { - q := ` - UPDATE report_config - SET time = :time, updated_at = :updated_at WHERE id = :id - RETURNING id, name, description, domain_id, config, metrics, - email, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status; - ` - - dbr := dbReport{ - 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.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) - } - defer row.Close() - - var dbReport dbReport - if row.Next() { - if err := row.StructScan(&dbReport); err != nil { - return re.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - } - report, err := dbToReport(dbReport) - if err != nil { - return re.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err) - } - - return report, nil -} - -func pageReportQuery(pm re.PageMeta) string { - var query []string - if pm.Status != re.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.time < :scheduled_before") - } - if pm.ScheduledAfter != nil { - query = append(query, "rc.time > :scheduled_after") - } - if pm.Name != "" { - query = append(query, "rc.name ILIKE '%' || :name || '%'") - } - - var q string - if len(query) > 0 { - q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - - return q -} diff --git a/re/postgres/rule.go b/re/postgres/rule.go index 12f5e3e04..9a53013ec 100644 --- a/re/postgres/rule.go +++ b/re/postgres/rule.go @@ -8,6 +8,7 @@ import ( "encoding/json" "time" + "github.com/absmach/magistrala/pkg/schedule" "github.com/absmach/magistrala/re" "github.com/absmach/supermq/pkg/errors" "github.com/lib/pq" @@ -15,46 +16,26 @@ import ( // dbRule represents the database structure for a Rule. type dbRule struct { - ID string `db:"id"` - Name string `db:"name"` - DomainID string `db:"domain_id"` - Metadata []byte `db:"metadata,omitempty"` - InputChannel string `db:"input_channel"` - InputTopic sql.NullString `db:"input_topic"` - LogicType re.ScriptType `db:"logic_type"` - LogicOutputs pq.Int32Array `db:"logic_output"` - LogicValue string `db:"logic_value"` - OutputChannel sql.NullString `db:"output_channel"` - OutputTopic sql.NullString `db:"output_topic"` - StartDateTime sql.NullTime `db:"start_datetime"` - Time sql.NullTime `db:"time"` - Recurring re.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"` -} - -// 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"` - Time sql.NullTime `db:"time"` - Recurring re.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"` - Config []byte `db:"config,omitempty"` - Metrics []byte `db:"metrics"` - Email []byte `db:"email"` + ID string `db:"id"` + Name string `db:"name"` + DomainID string `db:"domain_id"` + Metadata []byte `db:"metadata,omitempty"` + InputChannel string `db:"input_channel"` + InputTopic sql.NullString `db:"input_topic"` + LogicType re.ScriptType `db:"logic_type"` + LogicOutputs pq.Int32Array `db:"logic_output"` + LogicValue string `db:"logic_value"` + OutputChannel sql.NullString `db:"output_channel"` + OutputTopic sql.NullString `db:"output_topic"` + 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) { @@ -127,7 +108,7 @@ func dbToRule(dto dbRule) (re.Rule, error) { }, OutputChannel: fromNullString(dto.OutputChannel), OutputTopic: fromNullString(dto.OutputTopic), - Schedule: re.Schedule{ + Schedule: schedule.Schedule{ StartDateTime: dto.StartDateTime.Time, Time: dto.Time.Time, Recurring: dto.Recurring, @@ -141,109 +122,6 @@ func dbToRule(dto dbRule) (re.Rule, error) { }, nil } -func reportToDb(r re.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, - 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, - Config: config, - Metrics: metrics, - Email: email, - }, nil -} - -func dbToReport(dto dbReport) (re.ReportConfig, error) { - var config re.MetricConfig - if dto.Config != nil { - if err := json.Unmarshal(dto.Config, &config); err != nil { - return re.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - var email re.EmailSetting - if dto.Email != nil { - if err := json.Unmarshal(dto.Email, &email); err != nil { - return re.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - var metrics []re.ReqMetric - if dto.Metrics != nil { - if err := json.Unmarshal(dto.Metrics, &metrics); err != nil { - return re.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - - rpt := re.ReportConfig{ - ID: dto.ID, - Name: dto.Name, - Description: dto.Description, - DomainID: dto.DomainID, - Config: &config, - Metrics: metrics, - Schedule: re.Schedule{ - StartDateTime: dto.StartDateTime.Time, - Time: dto.Time.Time, - Recurring: dto.Recurring, - RecurringPeriod: dto.RecurringPeriod, - }, - Email: &email, - Status: dto.Status, - CreatedAt: dto.CreatedAt, - CreatedBy: dto.CreatedBy, - UpdatedAt: dto.UpdatedAt, - UpdatedBy: dto.UpdatedBy, - } - - return rpt, nil -} - func toNullString(value string) sql.NullString { if value == "" { return sql.NullString{Valid: false} diff --git a/re/rule.go b/re/rule.go index 19d4e25ae..12deae44d 100644 --- a/re/rule.go +++ b/re/rule.go @@ -5,15 +5,13 @@ package re import ( "encoding/json" - "log/slog" "strings" "time" + "github.com/absmach/magistrala/pkg/schedule" "github.com/absmach/supermq/pkg/errors" ) -var ErrInvalidRecurringType = errors.New("invalid recurring type") - const protocol = "nats" // ScriptOutput is the indicator for type of the logic @@ -80,25 +78,19 @@ type ( ) type Rule struct { - ID string `json:"id"` - Name string `json:"name"` - DomainID string `json:"domain"` - Metadata Metadata `json:"metadata,omitempty"` - InputChannel string `json:"input_channel"` - InputTopic string `json:"input_topic"` - Logic Script `json:"logic"` - OutputChannel string `json:"output_channel,omitempty"` - OutputTopic string `json:"output_topic,omitempty"` - Schedule Schedule `json:"schedule"` - 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"` -} - -type RunInfo struct { - Level slog.Level - Details []slog.Attr - Message string + ID string `json:"id"` + Name string `json:"name"` + DomainID string `json:"domain"` + Metadata Metadata `json:"metadata,omitempty"` + InputChannel string `json:"input_channel"` + InputTopic string `json:"input_topic"` + Logic Script `json:"logic"` + OutputChannel string `json:"output_channel,omitempty"` + OutputTopic string `json:"output_topic,omitempty"` + Schedule schedule.Schedule `json:"schedule"` + 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"` } diff --git a/re/service.go b/re/service.go index ece66a643..d59abc6d3 100644 --- a/re/service.go +++ b/re/service.go @@ -5,22 +5,20 @@ package re import ( "context" - "fmt" - "strings" "time" grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" - "github.com/absmach/magistrala/pkg/reltime" + "github.com/absmach/magistrala/pkg/emailer" + pkglog "github.com/absmach/magistrala/pkg/logger" + "github.com/absmach/magistrala/pkg/schedule" + "github.com/absmach/magistrala/pkg/ticker" "github.com/absmach/supermq" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" svcerr "github.com/absmach/supermq/pkg/errors/service" "github.com/absmach/supermq/pkg/messaging" - "github.com/absmach/supermq/pkg/transformers/senml" ) -const limit = 1000 - type Repository interface { AddRule(ctx context.Context, r Rule) (Rule, error) ViewRule(ctx context.Context, id string) (Rule, error) @@ -30,33 +28,24 @@ type Repository interface { UpdateRuleStatus(ctx context.Context, r Rule) (Rule, error) ListRules(ctx context.Context, pm PageMeta) (Page, error) UpdateRuleDue(ctx context.Context, id string, due time.Time) (Rule, error) - - 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) - ListReportsConfig(ctx context.Context, pm PageMeta) (ReportConfigPage, error) - UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, 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"` - 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"` - 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 *Recurring `json:"recurring,omitempty" db:"recurring"` // Filter by recurring type + Total uint64 `json:"total" db:"total"` + Offset uint64 `json:"offset" db:"offset"` + Limit uint64 `json:"limit" db:"limit"` + Dir string `json:"dir" db:"dir"` + 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"` + 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 } type Page struct { @@ -77,32 +66,22 @@ type Service interface { EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error) DisableRule(ctx context.Context, session authn.Session, id string) (Rule, error) - AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) - ViewReportConfig(ctx context.Context, session authn.Session, id string) (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) - - GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error) StartScheduler(ctx context.Context) error } type re struct { repo Repository - runInfo chan RunInfo + runInfo chan pkglog.RunInfo idp supermq.IDProvider rePubSub messaging.PubSub writersPub messaging.Publisher alarmsPub messaging.Publisher - ticker Ticker - email Emailer + ticker ticker.Ticker + email emailer.Emailer readers grpcReadersV1.ReadersServiceClient } -func NewService(repo Repository, runInfo chan RunInfo, idp supermq.IDProvider, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, tck Ticker, emailer Emailer, readers grpcReadersV1.ReadersServiceClient) Service { +func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp supermq.IDProvider, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient) Service { return &re{ repo: repo, idp: idp, @@ -229,377 +208,3 @@ func (re *re) DisableRule(ctx context.Context, session authn.Session, id string) func (re *re) Cancel() error { return nil } - -func (re *re) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - id, err := re.idp.ID() - if err != nil { - return ReportConfig{}, err - } - - now := time.Now() - 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 := re.repo.AddReportConfig(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return reportConfig, nil -} - -func (re *re) ViewReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) { - cfg, err := re.repo.ViewReportConfig(ctx, id) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - return cfg, nil -} - -func (re *re) UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { - cfg.UpdatedAt = time.Now().UTC() - cfg.UpdatedBy = session.UserID - reportConfig, err := re.repo.UpdateReportConfig(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return reportConfig, nil -} - -func (re *re) 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 := re.repo.UpdateReportSchedule(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return c, nil -} - -func (re *re) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { - if err := re.repo.RemoveReportConfig(ctx, id); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - return nil -} - -func (re *re) ListReportsConfig(ctx context.Context, session authn.Session, pm PageMeta) (ReportConfigPage, error) { - pm.Domain = session.DomainID - page, err := re.repo.ListReportsConfig(ctx, pm) - if err != nil { - return ReportConfigPage{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - return page, nil -} - -func (re *re) 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 = re.repo.UpdateReportConfigStatus(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return cfg, nil -} - -func (re *re) 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 = re.repo.UpdateReportConfigStatus(ctx, cfg) - if err != nil { - return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return cfg, nil -} - -func (re *re) GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error) { - config.DomainID = session.DomainID - - if config.Status != EnabledStatus { - return ReportPage{}, svcerr.ErrInvalidStatus - } - - reportPage, err := re.generateReport(ctx, config, action) - if err != nil { - return ReportPage{}, err - } - - return reportPage, nil -} - -func (re *re) generateReport(ctx context.Context, cfg ReportConfig, action ReportAction) (ReportPage, error) { - genReportFile, err := generateFileFunc(action, cfg.Config.FileFormat) - if err != nil { - return ReportPage{}, err - } - - agg := grpcReadersV1.Aggregation_AGGREGATION_UNSPECIFIED - switch cfg.Config.Aggregation.AggType { - case AggregationMAX: - agg = grpcReadersV1.Aggregation_MAX - case AggregationMIN: - agg = grpcReadersV1.Aggregation_MIN - case AggregationCOUNT: - agg = grpcReadersV1.Aggregation_COUNT - case AggregationAVG: - agg = grpcReadersV1.Aggregation_AVG - case AggregationSUM: - agg = grpcReadersV1.Aggregation_SUM - } - - 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 - } - pm := &grpcReadersV1.PageMetadata{ - Aggregation: agg, - Limit: limit, - From: float64(from.UnixMicro()), - 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 := re.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 := re.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(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 := re.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: from, - To: to, - Aggregation: cfg.Config.Aggregation, - Total: uint64(len(reports)), - Reports: reports, - }, nil - } -} - -func generateFileFunc(action ReportAction, format Format) (func(string, []Report) ([]byte, error), error) { - switch action { - case DownloadReport, EmailReport: - switch format { - case PDF: - return generatePDFReport, nil - case CSV: - return generateCSVReport, nil - default: - return nil, errors.New("file format not supported") - } - default: - return nil, nil - } -} - -func (re *re) 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 := re.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, - }) - } - - return groupedReports -} diff --git a/re/service_test.go b/re/service_test.go index 537204d0f..cd76cd839 100644 --- a/re/service_test.go +++ b/re/service_test.go @@ -11,6 +11,8 @@ import ( "github.com/0x6flab/namegenerator" "github.com/absmach/magistrala/internal/testsutil" + pkglog "github.com/absmach/magistrala/pkg/logger" + pkgSch "github.com/absmach/magistrala/pkg/schedule" "github.com/absmach/magistrala/re" "github.com/absmach/magistrala/re/mocks" readmocks "github.com/absmach/magistrala/readers/mocks" @@ -32,26 +34,15 @@ var ( ruleName = namegen.Generate() ruleID = testsutil.GenerateUUID(&testing.T{}) inputChannel = "test.channel" - schedule = re.Schedule{ + schedule = pkgSch.Schedule{ StartDateTime: time.Now().Add(-time.Hour), - Recurring: re.Daily, + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: time.Now().Add(-time.Hour), } - reportName = namegen.Generate() - rptConfig = re.ReportConfig{ - ID: testsutil.GenerateUUID(&testing.T{}), - Name: reportName, - DomainID: domainID, - Status: re.EnabledStatus, - Schedule: schedule, - CreatedBy: userID, - UpdatedBy: userID, - UpdatedAt: time.Now(), - } ) -func newService(t *testing.T, runInfo chan re.RunInfo) (re.Service, *mocks.Repository, *pubsubmocks.PubSub, *mocks.Ticker) { +func newService(t *testing.T, runInfo chan pkglog.RunInfo) (re.Service, *mocks.Repository, *pubsubmocks.PubSub, *mocks.Ticker) { repo := new(mocks.Repository) mockTicker := new(mocks.Ticker) idProvider := uuid.NewMock() @@ -62,7 +53,7 @@ func newService(t *testing.T, runInfo chan re.RunInfo) (re.Service, *mocks.Repos } func TestAddRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) ruleName := namegen.Generate() now := time.Now().Add(time.Hour) cases := []struct { @@ -81,8 +72,8 @@ func TestAddRule(t *testing.T) { rule: re.Rule{ Name: ruleName, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -91,8 +82,8 @@ func TestAddRule(t *testing.T) { Name: ruleName, ID: ruleID, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -111,8 +102,8 @@ func TestAddRule(t *testing.T) { rule: re.Rule{ Name: ruleName, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -137,7 +128,7 @@ func TestAddRule(t *testing.T) { } func TestViewRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) now := time.Now().Add(time.Hour) cases := []struct { @@ -158,8 +149,8 @@ func TestViewRule(t *testing.T) { Name: ruleName, ID: ruleID, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -195,7 +186,7 @@ func TestViewRule(t *testing.T) { } func TestUpdateRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) newName := namegen.Generate() now := time.Now().Add(time.Hour) @@ -216,8 +207,8 @@ func TestUpdateRule(t *testing.T) { Name: newName, ID: ruleID, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -229,8 +220,8 @@ func TestUpdateRule(t *testing.T) { Name: newName, ID: ruleID, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -252,8 +243,8 @@ func TestUpdateRule(t *testing.T) { Name: ruleName, ID: ruleID, InputChannel: inputChannel, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: now, }, @@ -280,7 +271,7 @@ func TestUpdateRule(t *testing.T) { } func TestListRules(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) numRules := 50 now := time.Now().Add(time.Hour) var rules []re.Rule @@ -292,8 +283,8 @@ func TestListRules(t *testing.T) { Status: re.EnabledStatus, CreatedAt: now, CreatedBy: userID, - Schedule: re.Schedule{ - Recurring: re.Daily, + Schedule: pkgSch.Schedule{ + Recurring: pkgSch.Daily, Time: now.Add(1 * time.Hour), RecurringPeriod: 1, StartDateTime: now.Add(-1 * time.Hour), @@ -385,7 +376,7 @@ func TestListRules(t *testing.T) { } func TestRemoveRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string @@ -425,7 +416,7 @@ func TestRemoveRule(t *testing.T) { } func TestEnableRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) now := time.Now() @@ -484,7 +475,7 @@ func TestEnableRule(t *testing.T) { } func TestDisableRule(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) + svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) now := time.Now() @@ -543,7 +534,7 @@ func TestDisableRule(t *testing.T) { } func TestHandle(t *testing.T) { - svc, repo, pubmocks, _ := newService(t, make(chan re.RunInfo)) + svc, repo, pubmocks, _ := newService(t, make(chan pkglog.RunInfo)) now := time.Now() scheduled := false cases := []struct { @@ -600,7 +591,6 @@ func TestHandle(t *testing.T) { } }) repoCall1 := pubmocks.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(tc.publishErr) - repoCall2 := repo.On("ListReportsConfig", mock.Anything, mock.Anything).Return(re.ReportConfigPage{}, nil) err = svc.Handle(tc.message) assert.Nil(t, err) @@ -609,424 +599,13 @@ func TestHandle(t *testing.T) { repoCall.Unset() repoCall1.Unset() - repoCall2.Unset() - }) - } -} - -func TestAddReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) - - cases := []struct { - desc string - session authn.Session - cfg re.ReportConfig - res re.ReportConfig - err error - }{ - { - desc: "Add report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: re.ReportConfig{ - Name: reportName, - Schedule: schedule, - }, - res: rptConfig, - err: nil, - }, - { - desc: "Add report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: re.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) - } - defer repoCall.Unset() - }) - } -} - -func TestViewReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - res re.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) - - 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 re.RunInfo)) - - newName := namegen.Generate() - now := time.Now().Add(time.Hour) - cases := []struct { - desc string - session authn.Session - cfg re.ReportConfig - res re.ReportConfig - err error - }{ - { - desc: "update report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - cfg: re.ReportConfig{ - Name: newName, - ID: rptConfig.ID, - Schedule: schedule, - }, - res: re.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: re.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 re.RunInfo)) - numConfigs := 50 - now := time.Now().Add(time.Hour) - var configs []re.ReportConfig - for i := 0; i < numConfigs; i++ { - c := re.ReportConfig{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - DomainID: domainID, - Status: re.EnabledStatus, - CreatedAt: now, - CreatedBy: userID, - Schedule: schedule, - } - configs = append(configs, c) - } - - cases := []struct { - desc string - session authn.Session - pageMeta re.PageMeta - res re.ReportConfigPage - err error - }{ - { - desc: "list report configs successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - pageMeta: re.PageMeta{}, - res: re.ReportConfigPage{ - PageMeta: re.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: re.PageMeta{ - Limit: 100, - }, - res: re.ReportConfigPage{ - PageMeta: re.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: re.PageMeta{ - Offset: 20, - Limit: 10, - }, - res: re.ReportConfigPage{ - PageMeta: re.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: re.PageMeta{}, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("ListReportsConfig", 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, _, _ := newService(t, make(chan re.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - err error - }{ - { - desc: "remove report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - err: nil, - }, - { - desc: "remove report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - err: svcerr.ErrRemoveEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err) - 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)) - defer repoCall.Unset() - }) - } -} - -func TestEnableReportConfig(t *testing.T) { - svc, repo, _, _ := newService(t, make(chan re.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - status re.Status - res re.ReportConfig - err error - }{ - { - desc: "enable report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: re.EnabledStatus, - res: rptConfig, - err: nil, - }, - { - desc: "enable report config with failed repo", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: re.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 re.RunInfo)) - - cases := []struct { - desc string - session authn.Session - id string - status re.Status - res re.ReportConfig - err error - }{ - { - desc: "disable report config successfully", - session: authn.Session{ - UserID: userID, - DomainID: domainID, - }, - id: rptConfig.ID, - status: re.DisabledStatus, - res: re.ReportConfig{ - ID: rptConfig.ID, - Name: rptConfig.Name, - DomainID: rptConfig.DomainID, - Status: re.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: re.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 TestStartScheduler(t *testing.T) { now := time.Now().Truncate(time.Minute) - ri := make(chan re.RunInfo) + ri := make(chan pkglog.RunInfo) svc, repo, _, ticker := newService(t, ri) ctxCases := []struct { @@ -1078,7 +657,6 @@ func TestStartScheduler(t *testing.T) { for _, tc := range ctxCases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("ListRules", mock.Anything, mock.Anything).Return(tc.page, tc.listErr) - repoCall1 := repo.On("ListReportsConfig", mock.Anything, mock.Anything).Return(re.ReportConfigPage{}, nil) tickChan := make(chan time.Time) tickCall := ticker.On("Tick").Return((<-chan time.Time)(tickChan)) tickCall1 := ticker.On("Stop").Return() @@ -1093,7 +671,6 @@ func TestStartScheduler(t *testing.T) { err := <-errc assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v but got %v", tc.err, err)) repoCall.Unset() - repoCall1.Unset() tickCall.Unset() tickCall1.Unset() }) diff --git a/reports/api/doc.go b/reports/api/doc.go new file mode 100644 index 000000000..2424852cc --- /dev/null +++ b/reports/api/doc.go @@ -0,0 +1,6 @@ +// 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 new file mode 100644 index 000000000..03191ba6d --- /dev/null +++ b/reports/api/endpoints.go @@ -0,0 +1,238 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "context" + + "github.com/absmach/magistrala/reports" + api "github.com/absmach/supermq/api/http" + "github.com/absmach/supermq/pkg/authn" + svcerr "github.com/absmach/supermq/pkg/errors/service" + "github.com/go-kit/kit/endpoint" +) + +func generateReportEndpoint(svc reports.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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, reports.ReportConfig{ + Name: req.Name, + DomainID: req.DomainID, + Config: req.Config, + Metrics: req.Metrics, + Email: req.Email, + }, 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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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) + if err != nil { + return viewReportConfigRes{}, err + } + + return viewReportConfigRes{ReportConfig: cfg}, nil + } +} + +func addReportConfigEndpoint(svc reports.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 interface{}) (interface{}, error) { + session, ok := ctx.Value(api.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 + } +} diff --git a/reports/api/endpoints_test.go b/reports/api/endpoints_test.go new file mode 100644 index 000000000..4b97ac5c5 --- /dev/null +++ b/reports/api/endpoints_test.go @@ -0,0 +1,813 @@ +// 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" + "github.com/absmach/magistrala/internal/testsutil" + pkgSch "github.com/absmach/magistrala/pkg/schedule" + "github.com/absmach/magistrala/reports" + "github.com/absmach/magistrala/reports/api" + "github.com/absmach/magistrala/reports/mocks" + apiutil "github.com/absmach/supermq/api/http/util" + "github.com/absmach/supermq/auth" + smqlog "github.com/absmach/supermq/logger" + smqauthn "github.com/absmach/supermq/pkg/authn" + authnmocks "github.com/absmach/supermq/pkg/authn/mocks" + "github.com/absmach/supermq/pkg/errors" + svcerr "github.com/absmach/supermq/pkg/errors/service" + "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) + schedule = pkgSch.Schedule{ + StartDateTime: now.Add(-1 * time.Hour), + Recurring: pkgSch.Daily, + RecurringPeriod: 1, + Time: now, + } + 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 := smqlog.NewMock() + mux := chi.NewRouter() + api.MakeHandler(svc, authn, 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() + + 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 service error", + token: validToken, + domainID: domainID, + authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}, + cfg: reportConfig, + contentType: contentType, + svcErr: svcerr.ErrAuthorization, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + } + + 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).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"` +} diff --git a/reports/api/request.go b/reports/api/request.go new file mode 100644 index 000000000..6a5445a1b --- /dev/null +++ b/reports/api/request.go @@ -0,0 +1,171 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "fmt" + + "github.com/absmach/magistrala/pkg/schedule" + "github.com/absmach/magistrala/reports" + apiutil "github.com/absmach/supermq/api/http/util" + "github.com/absmach/supermq/pkg/errors" + svcerr "github.com/absmach/supermq/pkg/errors/service" +) + +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") + errTitleSize = errors.New("invalid title size") +) + +type addReportConfigReq struct { + reports.ReportConfig `json:",inline"` +} + +func (req addReportConfigReq) validate() error { + if req.Name == "" { + return apiutil.ErrMissingName + } + return validateReportConfig(req.ReportConfig, false, false) +} + +type viewReportConfigReq struct { + ID string `json:"id"` +} + +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 + } + 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 + } + + 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) + } + + 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 errors.Wrap(apiutil.ErrValidation, errInvalidRecurringPeriod) + } + return nil +} diff --git a/reports/api/response.go b/reports/api/response.go new file mode 100644 index 000000000..9f3f01458 --- /dev/null +++ b/reports/api/response.go @@ -0,0 +1,167 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "net/http" + "time" + + "github.com/absmach/magistrala/reports" + "github.com/absmach/supermq" +) + +var ( + _ supermq.Response = (*addReportConfigRes)(nil) + _ supermq.Response = (*viewReportConfigRes)(nil) + _ supermq.Response = (*updateReportConfigRes)(nil) + _ supermq.Response = (*deleteReportConfigRes)(nil) + _ supermq.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.StatusCreated +} + +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 +} diff --git a/reports/api/transport.go b/reports/api/transport.go new file mode 100644 index 000000000..68ecb84d3 --- /dev/null +++ b/reports/api/transport.go @@ -0,0 +1,247 @@ +// 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/reports" + "github.com/absmach/supermq" + api "github.com/absmach/supermq/api/http" + apiutil "github.com/absmach/supermq/api/http/util" + mgauthn "github.com/absmach/supermq/pkg/authn" + "github.com/absmach/supermq/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" +) + +const ( + reportIdKey = "reportID" + statusKey = "status" + actionKey = "action" + defAction = "view" +) + +// MakeHandler creates an HTTP handler for the service endpoints. +func MakeHandler(svc reports.Service, authn mgauthn.Authentication, 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(api.AuthenticateMiddleware(authn, true)) + 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("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( + viewReportConfigEndpoint(svc), + decodeViewReportConfigRequest, + api.EncodeResponse, + opts..., + ), "view_report_config").ServeHTTP) + + r.Patch("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( + updateReportConfigEndpoint(svc), + decodeUpdateReportConfigRequest, + api.EncodeResponse, + opts..., + ), "update_report_config").ServeHTTP) + + r.Patch("/{reportID}/schedule", otelhttp.NewHandler(kithttp.NewServer( + updateReportScheduleEndpoint(svc), + decodeUpdateReportScheduleRequest, + api.EncodeResponse, + opts..., + ), "update_report_scheduler").ServeHTTP) + + r.Delete("/{reportID}", otelhttp.NewHandler(kithttp.NewServer( + deleteReportConfigEndpoint(svc), + decodeDeleteReportConfigRequest, + api.EncodeResponse, + opts..., + ), "delete_report_config").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + listReportsConfigEndpoint(svc), + decodeListReportsConfigRequest, + api.EncodeResponse, + opts..., + ), "list_reports_config").ServeHTTP) + + r.Post("/{reportID}/enable", otelhttp.NewHandler(kithttp.NewServer( + enableReportConfigEndpoint(svc), + decodeUpdateReportStatusRequest, + api.EncodeResponse, + opts..., + ), "enable_report_config").ServeHTTP) + + r.Post("/{reportID}/disable", otelhttp.NewHandler(kithttp.NewServer( + disableReportConfigEndpoint(svc), + decodeUpdateReportStatusRequest, + api.EncodeResponse, + opts..., + ), "disable_report_config").ServeHTTP) + }) + }) + }) + }) + + mux.Get("/health", supermq.Health("rule_engine", instanceID)) + mux.Handle("/metrics", promhttp.Handler()) + + return mux +} + +func decodeGenerateReportRequest(_ context.Context, r *http.Request) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Wrap(apiutil.ErrValidation, 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) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Wrap(apiutil.ErrValidation, 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) (interface{}, error) { + id := chi.URLParam(r, reportIdKey) + return viewReportConfigReq{ID: id}, nil +} + +func decodeUpdateReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Wrap(apiutil.ErrValidation, 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) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) + } + + req := updateReportScheduleReq{ + id: chi.URLParam(r, reportIdKey), + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return req, nil +} + +func decodeUpdateReportStatusRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := updateReportStatusReq{ + id: chi.URLParam(r, reportIdKey), + } + return req, nil +} + +func decodeDeleteReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) { + id := chi.URLParam(r, reportIdKey) + return deleteReportConfigReq{ID: id}, nil +} + +func decodeListReportsConfigRequest(_ context.Context, r *http.Request) (interface{}, 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) + } + return listReportsConfigReq{ + PageMeta: reports.PageMeta{ + Offset: offset, + Limit: limit, + Status: st, + Name: name, + }, + }, nil +} + +func encodeFileDownloadResponse(_ context.Context, w http.ResponseWriter, response interface{}) 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.(supermq.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/re/generator.go b/reports/generator.go similarity index 99% rename from re/generator.go rename to reports/generator.go index 0a08effac..ada7a7451 100644 --- a/re/generator.go +++ b/reports/generator.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package re +package reports import ( "bytes" diff --git a/reports/handler.go b/reports/handler.go new file mode 100644 index 000000000..64a54f3a1 --- /dev/null +++ b/reports/handler.go @@ -0,0 +1,64 @@ +// 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.ListReportsConfig(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 new file mode 100644 index 000000000..12e76d7bb --- /dev/null +++ b/reports/middleware/authorization.go @@ -0,0 +1,190 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + + "github.com/absmach/magistrala/reports" + "github.com/absmach/supermq/pkg/authn" + smqauthz "github.com/absmach/supermq/pkg/authz" + "github.com/absmach/supermq/pkg/errors" + "github.com/absmach/supermq/pkg/policies" +) + +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") +) + +type authorizationMiddleware struct { + svc reports.Service + authz smqauthz.Authorization +} + +// AuthorizationMiddleware adds authorization to the reports service. +func AuthorizationMiddleware(svc reports.Service, authz smqauthz.Authorization) (reports.Service, error) { + return &authorizationMiddleware{ + svc: svc, + authz: authz, + }, nil +} + +func (am *authorizationMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { + if err := am.authorize(ctx, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); 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) (reports.ReportConfig, error) { + if err := am.authorize(ctx, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); err != nil { + return reports.ReportConfig{}, errors.Wrap(errDomainViewConfigs, err) + } + + return am.svc.ViewReportConfig(ctx, session, id) +} + +func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { + if err := am.authorize(ctx, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); 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, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); err != nil { + return reports.ReportConfig{}, errors.Wrap(errDomainDeleteConfigs, 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, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); 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) { + if err := am.authorize(ctx, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); err != nil { + return reports.ReportConfigPage{}, errors.Wrap(errDomainViewConfigs, 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, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); 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, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); 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, smqauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }); err != nil { + return reports.ReportPage{}, errors.Wrap(errDomainGenerateReports, err) + } + + return am.svc.GenerateReport(ctx, session, config, action) +} + +func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error { + return am.svc.StartScheduler(ctx) +} + +func (am *authorizationMiddleware) authorize(ctx context.Context, pr smqauthz.PolicyReq) error { + if err := am.authz.Authorize(ctx, pr); err != nil { + return err + } + return nil +} diff --git a/reports/middleware/logging.go b/reports/middleware/logging.go new file mode 100644 index 000000000..5bc4fc7d8 --- /dev/null +++ b/reports/middleware/logging.go @@ -0,0 +1,210 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + "log/slog" + "time" + + "github.com/absmach/magistrala/reports" + "github.com/absmach/supermq/pkg/authn" +) + +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, 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) (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) +} + +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) +} diff --git a/reports/mocks/repository.go b/reports/mocks/repository.go new file mode 100644 index 000000000..4adf121a3 --- /dev/null +++ b/reports/mocks/repository.go @@ -0,0 +1,475 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify +// Copyright (c) Abstract Machines + +// SPDX-License-Identifier: Apache-2.0 + +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 +// - cfg +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) { + run(args[0].(context.Context), args[1].(reports.ReportConfig)) + }) + 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 +} + +// ListReportsConfig provides a mock function for the type Repository +func (_mock *Repository) ListReportsConfig(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error) { + ret := _mock.Called(ctx, 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, 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_ListReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReportsConfig' +type Repository_ListReportsConfig_Call struct { + *mock.Call +} + +// ListReportsConfig is a helper method to define mock.On call +// - ctx +// - pm +func (_e *Repository_Expecter) ListReportsConfig(ctx interface{}, pm interface{}) *Repository_ListReportsConfig_Call { + return &Repository_ListReportsConfig_Call{Call: _e.mock.On("ListReportsConfig", ctx, pm)} +} + +func (_c *Repository_ListReportsConfig_Call) Run(run func(ctx context.Context, pm reports.PageMeta)) *Repository_ListReportsConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(reports.PageMeta)) + }) + return _c +} + +func (_c *Repository_ListReportsConfig_Call) Return(reportConfigPage reports.ReportConfigPage, err error) *Repository_ListReportsConfig_Call { + _c.Call.Return(reportConfigPage, err) + return _c +} + +func (_c *Repository_ListReportsConfig_Call) RunAndReturn(run func(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error)) *Repository_ListReportsConfig_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 +// - id +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) { + run(args[0].(context.Context), args[1].(string)) + }) + 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 +// - cfg +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) { + run(args[0].(context.Context), args[1].(reports.ReportConfig)) + }) + 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 +// - cfg +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) { + run(args[0].(context.Context), args[1].(reports.ReportConfig)) + }) + 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 +// - id +// - due +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) { + run(args[0].(context.Context), args[1].(string), args[2].(time.Time)) + }) + 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 +// - cfg +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) { + run(args[0].(context.Context), args[1].(reports.ReportConfig)) + }) + 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 +} + +// 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 +// - id +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) { + run(args[0].(context.Context), args[1].(string)) + }) + 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 +} diff --git a/reports/mocks/service.go b/reports/mocks/service.go new file mode 100644 index 000000000..d4a9beb75 --- /dev/null +++ b/reports/mocks/service.go @@ -0,0 +1,584 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify +// Copyright (c) Abstract Machines + +// SPDX-License-Identifier: Apache-2.0 + +package mocks + +import ( + "context" + + "github.com/absmach/magistrala/reports" + "github.com/absmach/supermq/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} +} + +// 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 +// - session +// - cfg +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig)) + }) + 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 +} + +// 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 +// - session +// - id +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) + }) + 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 +// - session +// - id +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) + }) + 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 +// - session +// - config +// - action +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig), args[3].(reports.ReportAction)) + }) + 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 +// - session +// - pm +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.PageMeta)) + }) + 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 +// - session +// - id +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) + }) + 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 +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) { + run(args[0].(context.Context)) + }) + 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 +// - session +// - cfg +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig)) + }) + 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 +// - session +// - cfg +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) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig)) + }) + 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 +} + +// ViewReportConfig provides a mock function for the type Service +func (_mock *Service) ViewReportConfig(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 ViewReportConfig") + } + + 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_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 +// - session +// - id +func (_e *Service_Expecter) ViewReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_ViewReportConfig_Call { + return &Service_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, session, id)} +} + +func (_c *Service_ViewReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewReportConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(authn.Session), args[2].(string)) + }) + 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) (reports.ReportConfig, error)) *Service_ViewReportConfig_Call { + _c.Call.Return(run) + return _c +} diff --git a/reports/postgres/init.go b/reports/postgres/init.go new file mode 100644 index 000000000..c9ac2c232 --- /dev/null +++ b/reports/postgres/init.go @@ -0,0 +1,42 @@ +// 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: "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;`, + }, + }, + }, + } +} diff --git a/reports/postgres/reports.go b/reports/postgres/reports.go new file mode 100644 index 000000000..58ccf190d --- /dev/null +++ b/reports/postgres/reports.go @@ -0,0 +1,137 @@ +// 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"` +} + +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, + }, 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, + } + + return rpt, nil +} diff --git a/reports/postgres/repository.go b/reports/postgres/repository.go new file mode 100644 index 000000000..16ebc32f8 --- /dev/null +++ b/reports/postgres/repository.go @@ -0,0 +1,340 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/absmach/magistrala/pkg/errors" + "github.com/absmach/magistrala/reports" + repoerr "github.com/absmach/supermq/pkg/errors/repository" + "github.com/absmach/supermq/pkg/postgres" +) + +type PostgresRepository struct { + DB postgres.Database +} + +func NewRepository(db postgres.Database) reports.Repository { + return &PostgresRepository{DB: db} +} + +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) + VALUES (:id, :name, :description, :domain_id, :config, :metrics, + :email, :start_datetime, :due, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status) + 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{}, 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.StructScan(&dbReport); err != nil { + return reports.ReportConfig{}, err + } + } + + report, err := dbToReport(dbReport) + if err != nil { + return reports.ReportConfig{}, 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, + 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 { + 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 = fmt.Sprintf("%s", 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.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.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) ListReportsConfig(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; + ` + + pgData := "" + if pm.Limit != 0 { + pgData = "LIMIT :limit" + } + if pm.Offset != 0 { + pgData += " OFFSET :offset" + } + pq := pageReportQuery(pm) + q := fmt.Sprintf(listReportsQuery, pq, 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.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 pageReportQuery(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 || '%'") + } + + var q string + if len(query) > 0 { + q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) + } + + return q +} diff --git a/re/reports.go b/reports/reports.go similarity index 72% rename from re/reports.go rename to reports/reports.go index c1d4fd085..748928521 100644 --- a/re/reports.go +++ b/reports/reports.go @@ -1,9 +1,10 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package re +package reports import ( + "context" "encoding/json" "fmt" "net/mail" @@ -11,6 +12,8 @@ import ( "time" "github.com/absmach/magistrala/pkg/reltime" + "github.com/absmach/magistrala/pkg/schedule" + "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/pkg/transformers/senml" ) @@ -142,19 +145,19 @@ func (rm ReqMetric) Validate() error { } type ReportConfig struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - DomainID string `json:"domain_id"` - Schedule Schedule `json:"schedule,omitempty"` - Config *MetricConfig `json:"config,omitempty"` - Email *EmailSetting `json:"email,omitempty"` - Metrics []ReqMetric `json:"metrics,omitempty"` - Status Status `json:"status"` - CreatedAt time.Time `json:"created_at,omitempty"` - CreatedBy string `json:"created_by,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - UpdatedBy string `json:"updated_by,omitempty"` + 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"` + Status Status `json:"status"` + CreatedAt time.Time `json:"created_at,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` } type ReportConfigPage struct { @@ -370,3 +373,39 @@ func (a *Aggregation) UnmarshalJSON(data []byte) error { *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"` + 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 +} + +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) + ListReportsConfig(ctx context.Context, pm PageMeta) (ReportConfigPage, error) + UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, error) +} + +type Service interface { + AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) + ViewReportConfig(ctx context.Context, session authn.Session, id string) (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) + + 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 new file mode 100644 index 000000000..83776c40a --- /dev/null +++ b/reports/service.go @@ -0,0 +1,418 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package reports + +import ( + "context" + "fmt" + "strings" + "time" + + grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1" + "github.com/absmach/magistrala/pkg/emailer" + pkglog "github.com/absmach/magistrala/pkg/logger" + "github.com/absmach/magistrala/pkg/reltime" + "github.com/absmach/magistrala/pkg/ticker" + "github.com/absmach/supermq" + "github.com/absmach/supermq/pkg/authn" + "github.com/absmach/supermq/pkg/errors" + svcerr "github.com/absmach/supermq/pkg/errors/service" + "github.com/absmach/supermq/pkg/transformers/senml" +) + +const limit = 1000 + +type report struct { + repo Repository + runInfo chan pkglog.RunInfo + idp supermq.IDProvider + email emailer.Emailer + ticker ticker.Ticker + readers grpcReadersV1.ReadersServiceClient +} + +func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp supermq.IDProvider, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient) Service { + return &report{ + repo: repo, + idp: idp, + runInfo: runInfo, + email: emailer, + ticker: tck, + readers: readers, + } +} + +func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) { + id, err := r.idp.ID() + if err != nil { + return ReportConfig{}, err + } + + now := time.Now() + 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) + } + + return reportConfig, nil +} + +func (r *report) ViewReportConfig(ctx context.Context, session authn.Session, id string) (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.ListReportsConfig(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 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 := generateFileFunc(action, cfg.Config.FileFormat) + if err != nil { + return ReportPage{}, err + } + + agg := grpcReadersV1.Aggregation_AGGREGATION_UNSPECIFIED + switch cfg.Config.Aggregation.AggType { + case AggregationMAX: + agg = grpcReadersV1.Aggregation_MAX + case AggregationMIN: + agg = grpcReadersV1.Aggregation_MIN + case AggregationCOUNT: + agg = grpcReadersV1.Aggregation_COUNT + case AggregationAVG: + agg = grpcReadersV1.Aggregation_AVG + case AggregationSUM: + agg = grpcReadersV1.Aggregation_SUM + } + + 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 + } + pm := &grpcReadersV1.PageMetadata{ + Aggregation: agg, + Limit: limit, + From: float64(from.UnixMicro()), + 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(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: from, + To: to, + Aggregation: cfg.Config.Aggregation, + Total: uint64(len(reports)), + Reports: reports, + }, nil + } +} + +func generateFileFunc(action ReportAction, format Format) (func(string, []Report) ([]byte, error), error) { + switch action { + case DownloadReport, EmailReport: + switch format { + case PDF: + return generatePDFReport, nil + case CSV: + return generateCSVReport, 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, + }) + } + + return groupedReports +} diff --git a/reports/service_test.go b/reports/service_test.go new file mode 100644 index 000000000..0ed37bc04 --- /dev/null +++ b/reports/service_test.go @@ -0,0 +1,466 @@ +// 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" + pkglog "github.com/absmach/magistrala/pkg/logger" + pkgSch "github.com/absmach/magistrala/pkg/schedule" + remocks "github.com/absmach/magistrala/re/mocks" + readmocks "github.com/absmach/magistrala/readers/mocks" + "github.com/absmach/magistrala/reports" + "github.com/absmach/magistrala/reports/mocks" + "github.com/absmach/supermq/pkg/authn" + "github.com/absmach/supermq/pkg/errors" + repoerr "github.com/absmach/supermq/pkg/errors/repository" + svcerr "github.com/absmach/supermq/pkg/errors/service" + "github.com/absmach/supermq/pkg/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var ( + namegen = namegenerator.NewGenerator() + userID = testsutil.GenerateUUID(&testing.T{}) + domainID = testsutil.GenerateUUID(&testing.T{}) + schedule = pkgSch.Schedule{ + StartDateTime: time.Now().Add(-time.Hour), + 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(runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *remocks.Ticker) { + repo := new(mocks.Repository) + mockTicker := new(remocks.Ticker) + idProvider := uuid.NewMock() + readersSvc := new(readmocks.ReadersServiceClient) + e := new(remocks.Emailer) + return reports.NewService(repo, runInfo, idProvider, mockTicker, e, readersSvc), repo, mockTicker +} + +func TestAddReportConfig(t *testing.T) { + svc, repo, _ := newService(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) + } + defer repoCall.Unset() + }) + } +} + +func TestViewReportConfig(t *testing.T) { + svc, repo, _ := newService(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) + 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(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(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 + }{ + { + 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, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + repoCall := repo.On("ListReportsConfig", 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, _ := newService(make(chan pkglog.RunInfo)) + + cases := []struct { + desc string + session authn.Session + id string + err error + }{ + { + desc: "remove report config successfully", + session: authn.Session{ + UserID: userID, + DomainID: domainID, + }, + id: rptConfig.ID, + err: nil, + }, + { + desc: "remove report config with failed repo", + session: authn.Session{ + UserID: userID, + DomainID: domainID, + }, + id: rptConfig.ID, + err: svcerr.ErrRemoveEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err) + 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)) + defer repoCall.Unset() + }) + } +} + +func TestEnableReportConfig(t *testing.T) { + svc, repo, _ := newService(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(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() + }) + } +} diff --git a/reports/status.go b/reports/status.go new file mode 100644 index 000000000..b223f4793 --- /dev/null +++ b/reports/status.go @@ -0,0 +1,80 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package reports + +import ( + "encoding/json" + "strings" + + svcerr "github.com/absmach/supermq/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/tools/config/.mockery.yaml b/tools/config/.mockery.yaml index 7284c5fe0..294735b43 100644 --- a/tools/config/.mockery.yaml +++ b/tools/config/.mockery.yaml @@ -38,6 +38,10 @@ packages: interfaces: Service: Repository: + github.com/absmach/magistrala/reports: + interfaces: + Service: + Repository: github.com/absmach/magistrala/api/grpc/readers/v1: interfaces: ReadersServiceClient: