diff --git a/cmd/reports/main.go b/cmd/reports/main.go index fd3faae98..e243bc21b 100644 --- a/cmd/reports/main.go +++ b/cmd/reports/main.go @@ -45,6 +45,7 @@ import ( grpcClient "github.com/absmach/magistrala/readers/api/grpc" "github.com/absmach/magistrala/reports" httpapi "github.com/absmach/magistrala/reports/api" + reportsevents "github.com/absmach/magistrala/reports/events" "github.com/absmach/magistrala/reports/middleware" "github.com/absmach/magistrala/reports/operations" repg "github.com/absmach/magistrala/reports/postgres" @@ -285,7 +286,7 @@ func main() { runInfo := make(chan pkglog.RunInfo, channBuffer) - svc, err := newService(cfg, database, runInfo, authz, ec, logger, readersClient, template, callout, tracer) + svc, err := newService(ctx, cfg, database, runInfo, authz, ec, logger, readersClient, template, callout, tracer) if err != nil { logger.Error(fmt.Sprintf("failed to create services: %s", err)) exitCode = 1 @@ -325,7 +326,7 @@ func main() { } } -func newService(cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, callout callout.Callout, tracer trace.Tracer) (reports.Service, error) { +func newService(ctx context.Context, cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, callout callout.Callout, tracer trace.Tracer) (reports.Service, error) { repo := repg.NewRepository(db) idp := uuid.New() @@ -350,6 +351,11 @@ func newService(cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, a return nil, fmt.Errorf("failed to create reports service: %w", err) } + csvc, err = reportsevents.NewEventStoreMiddleware(ctx, csvc, cfg.ESURL) + if err != nil { + return nil, fmt.Errorf("failed to init reports event store middleware: %w", err) + } + permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile) if err != nil { return nil, fmt.Errorf("failed to parse permissions file: %w", err) diff --git a/docker/.env b/docker/.env index 385b94f8e..7e48eb8bb 100644 --- a/docker/.env +++ b/docker/.env @@ -578,6 +578,14 @@ MG_REPORTS_EMAIL_TEMPLATE=reports.tmpl MG_REPORTS_DEFAULT_TEMPLATE= MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html MG_REPORTS_URL=http://reports:9017 +MG_REPORTS_CALLOUT_URLS="" +MG_REPORTS_CALLOUT_METHOD="POST" +MG_REPORTS_CALLOUT_TLS_VERIFICATION="false" +MG_REPORTS_CALLOUT_TIMEOUT="10s" +MG_REPORTS_CALLOUT_CA_CERT="" +MG_REPORTS_CALLOUT_CERT="" +MG_REPORTS_CALLOUT_KEY="" +MG_REPORTS_CALLOUT_OPERATIONS="" ## Addon Services @@ -758,6 +766,14 @@ MG_REPORTS_EMAIL_TEMPLATE=reports.tmpl MG_REPORTS_DEFAULT_TEMPLATE= MG_REPORTS_URL=http://reports:9017 MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html +MG_REPORTS_CALLOUT_URLS="" +MG_REPORTS_CALLOUT_METHOD="POST" +MG_REPORTS_CALLOUT_TLS_VERIFICATION="false" +MG_REPORTS_CALLOUT_TIMEOUT="10s" +MG_REPORTS_CALLOUT_CA_CERT="" +MG_REPORTS_CALLOUT_CERT="" +MG_REPORTS_CALLOUT_KEY="" +MG_REPORTS_CALLOUT_OPERATIONS="" ### Timescale Reader gRPC Client Config (Magistrala) MG_TIMESCALE_READER_GRPC_URL=timescale-reader:7011 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index b3ae64d10..99ae41b71 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2139,6 +2139,14 @@ services: MG_REPORTS_DB_SSL_ROOT_CERT: ${MG_REPORTS_DB_SSL_ROOT_CERT} MG_REPORTS_DEFAULT_TEMPLATE: ${MG_REPORTS_DEFAULT_TEMPLATE} MG_PDF_CONVERTER_URL: ${MG_PDF_CONVERTER_URL} + MG_REPORTS_CALLOUT_URLS: ${MG_REPORTS_CALLOUT_URLS} + MG_REPORTS_CALLOUT_METHOD: ${MG_REPORTS_CALLOUT_METHOD} + MG_REPORTS_CALLOUT_TLS_VERIFICATION: ${MG_REPORTS_CALLOUT_TLS_VERIFICATION} + MG_REPORTS_CALLOUT_TIMEOUT: ${MG_REPORTS_CALLOUT_TIMEOUT} + MG_REPORTS_CALLOUT_CA_CERT: ${MG_REPORTS_CALLOUT_CA_CERT} + MG_REPORTS_CALLOUT_CERT: ${MG_REPORTS_CALLOUT_CERT} + MG_REPORTS_CALLOUT_KEY: ${MG_REPORTS_CALLOUT_KEY} + MG_REPORTS_CALLOUT_OPERATIONS: ${MG_REPORTS_CALLOUT_OPERATIONS} MG_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} MG_ES_URL: ${MG_ES_URL} MG_JAEGER_URL: ${MG_JAEGER_URL} diff --git a/re/events/events.go b/re/events/events.go index 4883e9893..a704fde1f 100644 --- a/re/events/events.go +++ b/re/events/events.go @@ -83,7 +83,7 @@ type listRuleEvent struct { // Encode implements the events.Event interface for listRuleEvent. func (lre listRuleEvent) Encode() (map[string]any, error) { - val := lre.PageMeta.EventEncode() + val := lre.EventEncode() maps.Copy(val, lre.baseRuleEvent.Encode()) val["operation"] = ruleList return val, nil diff --git a/re/events/streams_test.go b/re/events/streams_test.go new file mode 100644 index 000000000..b7f3954ad --- /dev/null +++ b/re/events/streams_test.go @@ -0,0 +1,580 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/messaging" + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/re" + "github.com/absmach/magistrala/re/events" + "github.com/absmach/magistrala/re/mocks" + "github.com/go-chi/chi/v5/middleware" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + storeClient *redis.Client + storeURL string + validSession = authn.Session{ + DomainID: testsutil.GenerateUUID(&testing.T{}), + UserID: testsutil.GenerateUUID(&testing.T{}), + } + validRule = generateTestRule(&testing.T{}) + validPage = re.Page{ + Offset: 0, + Limit: 10, + Total: 1, + Rules: []re.Rule{validRule}, + } +) + +func newEventStoreMiddleware(t *testing.T) (*mocks.Service, re.Service) { + svc := new(mocks.Service) + nsvc, err := events.NewEventStoreMiddleware(context.Background(), svc, storeURL) + require.Nil(t, err, fmt.Sprintf("create events store middleware failed with unexpected error: %s", err)) + + return svc, nsvc +} + +func TestMain(m *testing.M) { + code := testsutil.RunRedisTest(m, &storeClient, &storeURL) + os.Exit(code) +} + +func TestAddRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + rule re.Rule + svcRes re.Rule + svcRoleRes []roles.RoleProvision + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + rule: validRule, + svcRes: validRule, + svcRoleRes: []roles.RoleProvision{}, + svcErr: nil, + resp: validRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + rule: validRule, + svcRes: re.Rule{}, + svcRoleRes: []roles.RoleProvision{}, + svcErr: svcerr.ErrCreateEntity, + resp: re.Rule{}, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("AddRule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcRoleRes, tc.svcErr) + resp, _, err := nsvc.AddRule(validCtx, tc.session, tc.rule) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestViewRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + ruleID string + withRoles bool + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + ruleID: validRule.ID, + withRoles: false, + svcRes: validRule, + svcErr: nil, + resp: validRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + ruleID: validRule.ID, + withRoles: false, + svcRes: re.Rule{}, + svcErr: svcerr.ErrViewEntity, + resp: re.Rule{}, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("ViewRule", validCtx, tc.session, tc.ruleID, tc.withRoles).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.ViewRule(validCtx, tc.session, tc.ruleID, tc.withRoles) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + updatedRule := validRule + updatedRule.Name = "updatedName" + + cases := []struct { + desc string + session authn.Session + rule re.Rule + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + rule: updatedRule, + svcRes: updatedRule, + svcErr: nil, + resp: updatedRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + rule: updatedRule, + svcRes: re.Rule{}, + svcErr: svcerr.ErrUpdateEntity, + resp: re.Rule{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateRule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.UpdateRule(validCtx, tc.session, tc.rule) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateRuleTags(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + taggedRule := validRule + taggedRule.Tags = []string{"newtag1", "newtag2"} + + cases := []struct { + desc string + session authn.Session + rule re.Rule + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + rule: taggedRule, + svcRes: taggedRule, + svcErr: nil, + resp: taggedRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + rule: taggedRule, + svcRes: re.Rule{}, + svcErr: svcerr.ErrUpdateEntity, + resp: re.Rule{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateRuleTags", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.UpdateRuleTags(validCtx, tc.session, tc.rule) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateRuleSchedule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + rule re.Rule + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + rule: validRule, + svcRes: validRule, + svcErr: nil, + resp: validRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + rule: validRule, + svcRes: re.Rule{}, + svcErr: svcerr.ErrUpdateEntity, + resp: re.Rule{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateRuleSchedule", validCtx, tc.session, tc.rule).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.UpdateRuleSchedule(validCtx, tc.session, tc.rule) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestListRules(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + pageMeta re.PageMeta + svcRes re.Page + svcErr error + resp re.Page + err error + }{ + { + desc: "publish successfully", + session: validSession, + pageMeta: re.PageMeta{ + Limit: 10, + Offset: 0, + }, + svcRes: validPage, + svcErr: nil, + resp: validPage, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + pageMeta: re.PageMeta{ + Limit: 10, + Offset: 0, + }, + svcRes: re.Page{}, + svcErr: svcerr.ErrViewEntity, + resp: re.Page{}, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("ListRules", validCtx, tc.session, tc.pageMeta).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.ListRules(validCtx, tc.session, tc.pageMeta) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestRemoveRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + ruleID string + svcErr error + err error + }{ + { + desc: "publish successfully", + session: validSession, + ruleID: validRule.ID, + svcErr: nil, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + ruleID: validRule.ID, + svcErr: svcerr.ErrRemoveEntity, + err: svcerr.ErrRemoveEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("RemoveRule", validCtx, tc.session, tc.ruleID).Return(tc.svcErr) + err := nsvc.RemoveRule(validCtx, tc.session, tc.ruleID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestEnableRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + ruleID string + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + ruleID: validRule.ID, + svcRes: validRule, + svcErr: nil, + resp: validRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + ruleID: validRule.ID, + svcRes: re.Rule{}, + svcErr: svcerr.ErrUpdateEntity, + resp: re.Rule{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("EnableRule", validCtx, tc.session, tc.ruleID).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.EnableRule(validCtx, tc.session, tc.ruleID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestDisableRule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + ruleID string + svcRes re.Rule + svcErr error + resp re.Rule + err error + }{ + { + desc: "publish successfully", + session: validSession, + ruleID: validRule.ID, + svcRes: validRule, + svcErr: nil, + resp: validRule, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + ruleID: validRule.ID, + svcRes: re.Rule{}, + svcErr: svcerr.ErrUpdateEntity, + resp: re.Rule{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("DisableRule", validCtx, tc.session, tc.ruleID).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.DisableRule(validCtx, tc.session, tc.ruleID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestStartScheduler(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + cases := []struct { + desc string + svcErr error + err error + }{ + { + desc: "start scheduler successfully", + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + svcErr: svcerr.ErrCreateEntity, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("StartScheduler", context.Background()).Return(tc.svcErr) + err := nsvc.StartScheduler(context.Background()) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestHandle(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + msg := &messaging.Message{Channel: "test.channel"} + + cases := []struct { + desc string + msg *messaging.Message + svcErr error + err error + }{ + { + desc: "handle successfully", + msg: msg, + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + msg: msg, + svcErr: svcerr.ErrCreateEntity, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("Handle", tc.msg).Return(tc.svcErr) + err := nsvc.Handle(tc.msg) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestCancel(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + cases := []struct { + desc string + svcErr error + err error + }{ + { + desc: "cancel successfully", + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + svcErr: svcerr.ErrCreateEntity, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("Cancel").Return(tc.svcErr) + err := nsvc.Cancel() + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func generateTestRule(t *testing.T) re.Rule { + createdAt, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") + assert.Nil(t, err, fmt.Sprintf("Unexpected error parsing time: %v", err)) + return re.Rule{ + ID: testsutil.GenerateUUID(t), + Name: "testrule", + DomainID: testsutil.GenerateUUID(t), + InputChannel: "test.channel", + Status: re.EnabledStatus, + CreatedAt: createdAt, + UpdatedAt: createdAt, + } +} diff --git a/reports/README.md b/reports/README.md index 7cd072d37..a0820c9cc 100644 --- a/reports/README.md +++ b/reports/README.md @@ -85,6 +85,19 @@ The service is configured using the following environment variables (values show | `MG_REPORTS_DEFAULT_TEMPLATE` | Use on-disk HTML template when non-empty | "" | | `MG_PDF_CONVERTER_URL` | HTML-to-PDF conversion endpoint | `http://pdf-generator:3000/forms/chromium/convert/html` | +### Callout + +| Variable | Description | Default | +| --- | --- | --- | +| `MG_REPORTS_CALLOUT_URLS` | Callout target URLs | "" | +| `MG_REPORTS_CALLOUT_METHOD` | Callout HTTP method | `POST` | +| `MG_REPORTS_CALLOUT_TLS_VERIFICATION` | TLS verification for callout | `false` | +| `MG_REPORTS_CALLOUT_TIMEOUT` | Callout timeout | `10s` | +| `MG_REPORTS_CALLOUT_CA_CERT` | Callout CA cert path | "" | +| `MG_REPORTS_CALLOUT_CERT` | Callout client cert path | "" | +| `MG_REPORTS_CALLOUT_KEY` | Callout client key path | "" | +| `MG_REPORTS_CALLOUT_OPERATIONS` | Callout operations filter | "" | + ## Features - **Report generation**: Build report data from time-series messages. diff --git a/reports/events/doc.go b/reports/events/doc.go new file mode 100644 index 000000000..999dbec24 --- /dev/null +++ b/reports/events/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package events provides the domain concept definitions needed to support +// reports events functionality. +package events diff --git a/reports/events/events.go b/reports/events/events.go new file mode 100644 index 000000000..6f3e506c7 --- /dev/null +++ b/reports/events/events.go @@ -0,0 +1,68 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/events" + "github.com/absmach/magistrala/reports" +) + +const ( + reportPrefix = "report." + reportCreate = reportPrefix + "create" + reportRemove = reportPrefix + "remove" +) + +var ( + _ events.Event = (*createReportConfigEvent)(nil) + _ events.Event = (*removeReportConfigEvent)(nil) +) + +type baseReportEvent struct { + session authn.Session + requestID string +} + +func newBaseReportEvent(session authn.Session, requestID string) baseReportEvent { + return baseReportEvent{ + session: session, + requestID: requestID, + } +} + +func (bre baseReportEvent) Encode() map[string]any { + return map[string]any{ + "domain": bre.session.DomainID, + "user_id": bre.session.UserID, + "token_type": bre.session.Type.String(), + "super_admin": bre.session.SuperAdmin, + "request_id": bre.requestID, + } +} + +type createReportConfigEvent struct { + cfg reports.ReportConfig + baseReportEvent +} + +func (e createReportConfigEvent) Encode() (map[string]any, error) { + val := e.baseReportEvent.Encode() + val["id"] = e.cfg.ID + val["name"] = e.cfg.Name + val["operation"] = reportCreate + return val, nil +} + +type removeReportConfigEvent struct { + id string + baseReportEvent +} + +func (e removeReportConfigEvent) Encode() (map[string]any, error) { + val := e.baseReportEvent.Encode() + val["id"] = e.id + val["operation"] = reportRemove + return val, nil +} diff --git a/reports/events/streams.go b/reports/events/streams.go new file mode 100644 index 000000000..174264341 --- /dev/null +++ b/reports/events/streams.go @@ -0,0 +1,114 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/events" + "github.com/absmach/magistrala/pkg/events/store" + rmEvents "github.com/absmach/magistrala/pkg/roles/rolemanager/events" + "github.com/absmach/magistrala/reports" + "github.com/go-chi/chi/v5/middleware" +) + +const ( + magistralaPrefix = "magistrala." + CreateStream = magistralaPrefix + reportCreate + RemoveStream = magistralaPrefix + reportRemove +) + +var _ reports.Service = (*eventStore)(nil) + +type eventStore struct { + events.Publisher + svc reports.Service + rmEvents.RoleManagerEventStore +} + +func NewEventStoreMiddleware(ctx context.Context, svc reports.Service, url string) (reports.Service, error) { + publisher, err := store.NewPublisher(ctx, url, "reports-es-pub") + if err != nil { + return nil, err + } + + res := rmEvents.NewRoleManagerEventStore("reports", reportPrefix, svc, publisher) + + return &eventStore{ + svc: svc, + Publisher: publisher, + RoleManagerEventStore: res, + }, nil +} + +func (es *eventStore) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { + reportCfg, err := es.svc.AddReportConfig(ctx, session, cfg) + if err != nil { + return reportCfg, err + } + event := createReportConfigEvent{ + cfg: reportCfg, + baseReportEvent: newBaseReportEvent(session, middleware.GetReqID(ctx)), + } + if err := es.Publish(ctx, CreateStream, event); err != nil { + return reportCfg, err + } + return reportCfg, nil +} + +func (es *eventStore) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error { + if err := es.svc.RemoveReportConfig(ctx, session, id); err != nil { + return err + } + event := removeReportConfigEvent{ + id: id, + baseReportEvent: newBaseReportEvent(session, middleware.GetReqID(ctx)), + } + return es.Publish(ctx, RemoveStream, event) +} + +func (es *eventStore) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) { + return es.svc.ViewReportConfig(ctx, session, id, withRoles) +} + +func (es *eventStore) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { + return es.svc.UpdateReportConfig(ctx, session, cfg) +} + +func (es *eventStore) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) { + return es.svc.UpdateReportSchedule(ctx, session, cfg) +} + +func (es *eventStore) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) { + return es.svc.ListReportsConfig(ctx, session, pm) +} + +func (es *eventStore) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { + return es.svc.EnableReportConfig(ctx, session, id) +} + +func (es *eventStore) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) { + return es.svc.DisableReportConfig(ctx, session, id) +} + +func (es *eventStore) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error { + return es.svc.UpdateReportTemplate(ctx, session, cfg) +} + +func (es *eventStore) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) { + return es.svc.ViewReportTemplate(ctx, session, id) +} + +func (es *eventStore) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error { + return es.svc.DeleteReportTemplate(ctx, session, id) +} + +func (es *eventStore) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) { + return es.svc.GenerateReport(ctx, session, config, action) +} + +func (es *eventStore) StartScheduler(ctx context.Context) error { + return es.svc.StartScheduler(ctx) +} diff --git a/reports/events/streams_test.go b/reports/events/streams_test.go new file mode 100644 index 000000000..8c86d850f --- /dev/null +++ b/reports/events/streams_test.go @@ -0,0 +1,632 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/reports" + "github.com/absmach/magistrala/reports/events" + "github.com/absmach/magistrala/reports/mocks" + "github.com/go-chi/chi/v5/middleware" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + storeClient *redis.Client + storeURL string + validSession = authn.Session{ + DomainID: testsutil.GenerateUUID(&testing.T{}), + UserID: testsutil.GenerateUUID(&testing.T{}), + } + validReportConfig = generateTestReportConfig(&testing.T{}) + validReportConfigPage = reports.ReportConfigPage{ + PageMeta: reports.PageMeta{ + Limit: 10, + Offset: 0, + Total: 1, + }, + ReportConfigs: []reports.ReportConfig{validReportConfig}, + } +) + +func newEventStoreMiddleware(t *testing.T) (*mocks.Service, reports.Service) { + svc := new(mocks.Service) + nsvc, err := events.NewEventStoreMiddleware(context.Background(), svc, storeURL) + require.Nil(t, err, fmt.Sprintf("create events store middleware failed with unexpected error: %s", err)) + + return svc, nsvc +} + +func TestMain(m *testing.M) { + code := testsutil.RunRedisTest(m, &storeClient, &storeURL) + os.Exit(code) +} + +func TestAddReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + cfg reports.ReportConfig + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "publish successfully", + session: validSession, + cfg: validReportConfig, + svcRes: validReportConfig, + svcErr: nil, + resp: validReportConfig, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + cfg: validReportConfig, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrCreateEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("AddReportConfig", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.AddReportConfig(validCtx, tc.session, tc.cfg) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestRemoveReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + svcErr error + err error + }{ + { + desc: "publish successfully", + session: validSession, + reportID: validReportConfig.ID, + svcErr: nil, + err: nil, + }, + { + desc: "failed to publish with service error", + session: validSession, + reportID: validReportConfig.ID, + svcErr: svcerr.ErrRemoveEntity, + err: svcerr.ErrRemoveEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("RemoveReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcErr) + err := nsvc.RemoveReportConfig(validCtx, tc.session, tc.reportID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestViewReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + withRoles bool + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "view successfully", + session: validSession, + reportID: validReportConfig.ID, + withRoles: false, + svcRes: validReportConfig, + svcErr: nil, + resp: validReportConfig, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + reportID: validReportConfig.ID, + withRoles: false, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrViewEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("ViewReportConfig", validCtx, tc.session, tc.reportID, tc.withRoles).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.ViewReportConfig(validCtx, tc.session, tc.reportID, tc.withRoles) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + updatedCfg := validReportConfig + updatedCfg.Name = "updatedName" + + cases := []struct { + desc string + session authn.Session + cfg reports.ReportConfig + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "update successfully", + session: validSession, + cfg: updatedCfg, + svcRes: updatedCfg, + svcErr: nil, + resp: updatedCfg, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + cfg: updatedCfg, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrUpdateEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateReportConfig", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.UpdateReportConfig(validCtx, tc.session, tc.cfg) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateReportSchedule(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + cfg reports.ReportConfig + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "update schedule successfully", + session: validSession, + cfg: validReportConfig, + svcRes: validReportConfig, + svcErr: nil, + resp: validReportConfig, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + cfg: validReportConfig, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrUpdateEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateReportSchedule", validCtx, tc.session, tc.cfg).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.UpdateReportSchedule(validCtx, tc.session, tc.cfg) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestListReportsConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + pageMeta reports.PageMeta + svcRes reports.ReportConfigPage + svcErr error + resp reports.ReportConfigPage + err error + }{ + { + desc: "list successfully", + session: validSession, + pageMeta: reports.PageMeta{ + Limit: 10, + Offset: 0, + }, + svcRes: validReportConfigPage, + svcErr: nil, + resp: validReportConfigPage, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + pageMeta: reports.PageMeta{ + Limit: 10, + Offset: 0, + }, + svcRes: reports.ReportConfigPage{}, + svcErr: svcerr.ErrViewEntity, + resp: reports.ReportConfigPage{}, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("ListReportsConfig", validCtx, tc.session, tc.pageMeta).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.ListReportsConfig(validCtx, tc.session, tc.pageMeta) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestEnableReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "enable successfully", + session: validSession, + reportID: validReportConfig.ID, + svcRes: validReportConfig, + svcErr: nil, + resp: validReportConfig, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + reportID: validReportConfig.ID, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrUpdateEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("EnableReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.EnableReportConfig(validCtx, tc.session, tc.reportID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestDisableReportConfig(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + svcRes reports.ReportConfig + svcErr error + resp reports.ReportConfig + err error + }{ + { + desc: "disable successfully", + session: validSession, + reportID: validReportConfig.ID, + svcRes: validReportConfig, + svcErr: nil, + resp: validReportConfig, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + reportID: validReportConfig.ID, + svcRes: reports.ReportConfig{}, + svcErr: svcerr.ErrUpdateEntity, + resp: reports.ReportConfig{}, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("DisableReportConfig", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.DisableReportConfig(validCtx, tc.session, tc.reportID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestUpdateReportTemplate(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + cfg reports.ReportConfig + svcErr error + err error + }{ + { + desc: "update template successfully", + session: validSession, + cfg: validReportConfig, + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + cfg: validReportConfig, + svcErr: svcerr.ErrUpdateEntity, + err: svcerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("UpdateReportTemplate", validCtx, tc.session, tc.cfg).Return(tc.svcErr) + err := nsvc.UpdateReportTemplate(validCtx, tc.session, tc.cfg) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestViewReportTemplate(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + svcRes reports.ReportTemplate + svcErr error + resp reports.ReportTemplate + err error + }{ + { + desc: "view template successfully", + session: validSession, + reportID: validReportConfig.ID, + svcRes: reports.ReportTemplate("template content"), + svcErr: nil, + resp: reports.ReportTemplate("template content"), + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + reportID: validReportConfig.ID, + svcRes: reports.ReportTemplate(""), + svcErr: svcerr.ErrViewEntity, + resp: reports.ReportTemplate(""), + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("ViewReportTemplate", validCtx, tc.session, tc.reportID).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.ViewReportTemplate(validCtx, tc.session, tc.reportID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestDeleteReportTemplate(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + reportID string + svcErr error + err error + }{ + { + desc: "delete template successfully", + session: validSession, + reportID: validReportConfig.ID, + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + reportID: validReportConfig.ID, + svcErr: svcerr.ErrRemoveEntity, + err: svcerr.ErrRemoveEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("DeleteReportTemplate", validCtx, tc.session, tc.reportID).Return(tc.svcErr) + err := nsvc.DeleteReportTemplate(validCtx, tc.session, tc.reportID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func TestGenerateReport(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + validCtx := context.WithValue(context.Background(), middleware.RequestIDKey, testsutil.GenerateUUID(t)) + + cases := []struct { + desc string + session authn.Session + config reports.ReportConfig + action reports.ReportAction + svcRes reports.ReportPage + svcErr error + resp reports.ReportPage + err error + }{ + { + desc: "generate report successfully", + session: validSession, + config: validReportConfig, + action: reports.ViewReport, + svcRes: reports.ReportPage{}, + svcErr: nil, + resp: reports.ReportPage{}, + err: nil, + }, + { + desc: "failed with service error", + session: validSession, + config: validReportConfig, + action: reports.ViewReport, + svcRes: reports.ReportPage{}, + svcErr: svcerr.ErrViewEntity, + resp: reports.ReportPage{}, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("GenerateReport", validCtx, tc.session, tc.config, tc.action).Return(tc.svcRes, tc.svcErr) + resp, err := nsvc.GenerateReport(validCtx, tc.session, tc.config, tc.action) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) + svcCall.Unset() + }) + } +} + +func TestStartScheduler(t *testing.T) { + svc, nsvc := newEventStoreMiddleware(t) + + cases := []struct { + desc string + svcErr error + err error + }{ + { + desc: "start scheduler successfully", + svcErr: nil, + err: nil, + }, + { + desc: "failed with service error", + svcErr: svcerr.ErrCreateEntity, + err: svcerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := svc.On("StartScheduler", context.Background()).Return(tc.svcErr) + err := nsvc.StartScheduler(context.Background()) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + }) + } +} + +func generateTestReportConfig(t *testing.T) reports.ReportConfig { + createdAt, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") + assert.Nil(t, err, fmt.Sprintf("Unexpected error parsing time: %v", err)) + return reports.ReportConfig{ + ID: testsutil.GenerateUUID(t), + Name: "testreport", + DomainID: testsutil.GenerateUUID(t), + Status: reports.EnabledStatus, + CreatedAt: createdAt, + UpdatedAt: createdAt, + } +}