// Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 package reports_test import ( "context" "fmt" "testing" "time" "github.com/0x6flab/namegenerator" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/authn" emocks "github.com/absmach/magistrala/pkg/emailer/mocks" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" pkglog "github.com/absmach/magistrala/pkg/logger" policymocks "github.com/absmach/magistrala/pkg/policies/mocks" "github.com/absmach/magistrala/pkg/roles" pkgSch "github.com/absmach/magistrala/pkg/schedule" tmocks "github.com/absmach/magistrala/pkg/ticker/mocks" "github.com/absmach/magistrala/pkg/uuid" readmocks "github.com/absmach/magistrala/readers/mocks" "github.com/absmach/magistrala/reports" "github.com/absmach/magistrala/reports/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) var ( namegen = namegenerator.NewGenerator() userID = testsutil.GenerateUUID(&testing.T{}) domainID = testsutil.GenerateUUID(&testing.T{}) now = time.Now().UTC() template = reports.ReportTemplate("") schedule = pkgSch.Schedule{ StartDateTime: now, Recurring: pkgSch.Daily, RecurringPeriod: 1, Time: time.Now().Add(-time.Hour), } reportName = namegen.Generate() rptConfig = reports.ReportConfig{ ID: testsutil.GenerateUUID(&testing.T{}), Name: reportName, DomainID: domainID, Status: reports.EnabledStatus, Schedule: schedule, CreatedBy: userID, UpdatedBy: userID, UpdatedAt: time.Now(), } ) func newService(t *testing.T, runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *tmocks.Ticker, *policymocks.Service) { repo := new(mocks.Repository) mockTicker := new(tmocks.Ticker) idProvider := uuid.NewMock() readersSvc := new(readmocks.ReadersServiceClient) e := new(emocks.Emailer) policy := new(policymocks.Service) availableActions := []roles.Action{} builtInRoles := map[roles.BuiltInRoleName][]roles.Action{ "admin": availableActions, } svc, err := reports.NewService(repo, runInfo, policy, idProvider, mockTicker, e, readersSvc, template, "", availableActions, builtInRoles) if err != nil { t.Fatalf("Failed to create service: %v", err) } return svc, repo, mockTicker, policy } func TestAddReportConfig(t *testing.T) { svc, repo, _, policies := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string session authn.Session cfg reports.ReportConfig res reports.ReportConfig err error addPoliciesErr error deletePolicies error addRoleErr error deleteErr error }{ { desc: "Add report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: reportName, Schedule: schedule, }, res: rptConfig, err: nil, addPoliciesErr: nil, addRoleErr: nil, deleteErr: 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, addPoliciesErr: nil, deletePolicies: nil, addRoleErr: nil, deleteErr: nil, }, { desc: "Add report config with failed to add policies", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: reportName, Schedule: schedule, }, res: rptConfig, addPoliciesErr: svcerr.ErrAuthorization, err: svcerr.ErrAddPolicies, }, { desc: "Add report config with failed to add policies and failed rollback", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: reportName, Schedule: schedule, }, res: rptConfig, addPoliciesErr: svcerr.ErrAuthorization, deleteErr: svcerr.ErrRemoveEntity, err: svcerr.ErrRollbackRepo, }, { desc: "Add report config with failed to add roles", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: reportName, Schedule: schedule, }, res: rptConfig, addRoleErr: svcerr.ErrCreateEntity, err: svcerr.ErrAddPolicies, }, { desc: "Add report config with failed to add roles and failed to delete policies", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: reportName, Schedule: schedule, }, res: rptConfig, addRoleErr: svcerr.ErrCreateEntity, deletePolicies: svcerr.ErrRemoveEntity, err: svcerr.ErrRemoveEntity, }, } 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) policyCall := policies.On("AddPolicies", context.Background(), mock.Anything).Return(tc.addPoliciesErr) policyCall2 := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePolicies) repoCall1 := repo.On("AddRoles", context.Background(), mock.Anything).Return([]roles.RoleProvision{}, tc.addRoleErr) repoCall2 := repo.On("Remove", context.Background(), mock.Anything).Return(tc.deleteErr) 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) } policyCall.Unset() policyCall2.Unset() repoCall.Unset() repoCall1.Unset() repoCall2.Unset() }) } } func TestViewReportConfig(t *testing.T) { svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string session authn.Session id string res reports.ReportConfig err error }{ { desc: "view report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, res: rptConfig, err: nil, }, { desc: "view report config with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, err: svcerr.ErrViewEntity, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("ViewReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) res, err := svc.ViewReportConfig(context.Background(), tc.session, tc.id, false) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.Equal(t, tc.res, res) } defer repoCall.Unset() }) } } func TestUpdateReportConfig(t *testing.T) { svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) newName := namegen.Generate() now := time.Now().Add(time.Hour) cases := []struct { desc string session authn.Session cfg reports.ReportConfig res reports.ReportConfig err error }{ { desc: "update report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: newName, ID: rptConfig.ID, Schedule: schedule, }, res: reports.ReportConfig{ Name: newName, ID: rptConfig.ID, DomainID: rptConfig.DomainID, Status: rptConfig.Status, Schedule: rptConfig.Schedule, UpdatedAt: now, UpdatedBy: userID, }, err: nil, }, { desc: "update report config with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, cfg: reports.ReportConfig{ Name: rptConfig.Name, ID: rptConfig.ID, Schedule: schedule, }, err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("UpdateReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) res, err := svc.UpdateReportConfig(context.Background(), tc.session, tc.cfg) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.Equal(t, tc.res, res) } defer repoCall.Unset() }) } } func TestListReportsConfig(t *testing.T) { svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) numConfigs := 50 now := time.Now().Add(time.Hour) var configs []reports.ReportConfig for i := 0; i < numConfigs; i++ { c := reports.ReportConfig{ ID: testsutil.GenerateUUID(t), Name: namegen.Generate(), DomainID: domainID, Status: reports.EnabledStatus, CreatedAt: now, CreatedBy: userID, Schedule: schedule, } configs = append(configs, c) } cases := []struct { desc string session authn.Session pageMeta reports.PageMeta res reports.ReportConfigPage err error superAdmin bool }{ { desc: "list report configs successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, pageMeta: reports.PageMeta{}, res: reports.ReportConfigPage{ PageMeta: reports.PageMeta{ Total: uint64(numConfigs), Offset: 0, Limit: 10, }, ReportConfigs: configs[0:10], }, err: nil, }, { desc: "list report configs successfully with limit", session: authn.Session{ UserID: userID, DomainID: domainID, }, pageMeta: reports.PageMeta{ Limit: 100, }, res: reports.ReportConfigPage{ PageMeta: reports.PageMeta{ Total: uint64(numConfigs), Offset: 0, Limit: 100, }, ReportConfigs: configs[0:numConfigs], }, err: nil, }, { desc: "list report configs successfully with offset", session: authn.Session{ UserID: userID, DomainID: domainID, }, pageMeta: reports.PageMeta{ Offset: 20, Limit: 10, }, res: reports.ReportConfigPage{ PageMeta: reports.PageMeta{ Total: uint64(numConfigs), Offset: 20, Limit: 10, }, ReportConfigs: configs[20:30], }, err: nil, }, { desc: "list report configs with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, pageMeta: reports.PageMeta{}, err: svcerr.ErrViewEntity, }, { desc: "list report configs as super admin successfully", session: authn.Session{ UserID: userID, DomainID: domainID, SuperAdmin: true, }, pageMeta: reports.PageMeta{}, res: reports.ReportConfigPage{ PageMeta: reports.PageMeta{ Total: uint64(numConfigs), Offset: 0, Limit: 10, }, ReportConfigs: configs[0:10], }, superAdmin: true, err: nil, }, { desc: "list report configs as super admin with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, SuperAdmin: true, }, pageMeta: reports.PageMeta{}, superAdmin: true, err: svcerr.ErrViewEntity, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { var repoCall *mock.Call if tc.superAdmin { repoCall = repo.On("ListAllReportsConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err) } else { repoCall = repo.On("ListUserReportsConfig", mock.Anything, mock.Anything, mock.Anything).Return(tc.res, tc.err) } res, err := svc.ListReportsConfig(context.Background(), tc.session, tc.pageMeta) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.Equal(t, tc.res, res) } defer repoCall.Unset() }) } } func TestRemoveReportConfig(t *testing.T) { svc, repo, _, policies := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string session authn.Session id string err error deletePoliciesErr error }{ { desc: "remove report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, err: nil, deletePoliciesErr: nil, }, { desc: "remove report config with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, err: svcerr.ErrRemoveEntity, deletePoliciesErr: nil, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err) policyCall := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePoliciesErr) err := svc.RemoveReportConfig(context.Background(), tc.session, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) policyCall.Unset() repoCall.Unset() }) } } func TestEnableReportConfig(t *testing.T) { svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string session authn.Session id string status reports.Status res reports.ReportConfig err error }{ { desc: "enable report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, status: reports.EnabledStatus, res: rptConfig, err: nil, }, { desc: "enable report config with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, status: reports.EnabledStatus, err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("UpdateReportConfigStatus", context.Background(), mock.Anything).Return(tc.res, tc.err) res, err := svc.EnableReportConfig(context.Background(), tc.session, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.Equal(t, tc.res, res) } defer repoCall.Unset() }) } } func TestDisableReportConfig(t *testing.T) { svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo)) cases := []struct { desc string session authn.Session id string status reports.Status res reports.ReportConfig err error }{ { desc: "disable report config successfully", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, status: reports.DisabledStatus, res: reports.ReportConfig{ ID: rptConfig.ID, Name: rptConfig.Name, DomainID: rptConfig.DomainID, Status: reports.DisabledStatus, Schedule: schedule, UpdatedBy: userID, UpdatedAt: time.Now(), }, err: nil, }, { desc: "disable report config with failed repo", session: authn.Session{ UserID: userID, DomainID: domainID, }, id: rptConfig.ID, status: reports.DisabledStatus, err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { repoCall := repo.On("UpdateReportConfigStatus", mock.Anything, mock.Anything).Return(tc.res, tc.err) res, err := svc.DisableReportConfig(context.Background(), tc.session, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.Equal(t, tc.res, res) } defer repoCall.Unset() }) } } func TestGenerateInstantEmailReport(t *testing.T) { // nolint:dogsled svc, _, _, _ := newService(t, make(chan pkglog.RunInfo)) validEmailConfig := reports.EmailSetting{ To: []string{"test@example.com"}, Subject: "Test Report", Content: "Please find the attached report.", } validConfig := reports.ReportConfig{ ID: testsutil.GenerateUUID(&testing.T{}), Name: "Test Report", DomainID: domainID, Status: reports.DisabledStatus, Email: &validEmailConfig, Config: &reports.MetricConfig{ Title: "Test Report", FileFormat: reports.PDF, From: "now-1h", To: "now", Aggregation: reports.AggConfig{ AggType: reports.AggregationAVG, Interval: "1h", }, }, Metrics: []reports.ReqMetric{ { ChannelID: testsutil.GenerateUUID(&testing.T{}), Name: "temperature", ClientIDs: []string{testsutil.GenerateUUID(&testing.T{})}, }, }, ReportTemplate: template, } cases := []struct { desc string session authn.Session config reports.ReportConfig action reports.ReportAction err error }{ { desc: "Generate instant email report with disabled config should succeed", session: authn.Session{ UserID: userID, DomainID: domainID, }, config: validConfig, action: reports.EmailReport, err: nil, }, { desc: "Generate instant email report with enabled config should succeed", session: authn.Session{ UserID: userID, DomainID: domainID, }, config: func() reports.ReportConfig { cfg := validConfig cfg.Status = reports.EnabledStatus return cfg }(), action: reports.EmailReport, err: nil, }, { desc: "Generate view report with disabled config should succeed", session: authn.Session{ UserID: userID, DomainID: domainID, }, config: validConfig, action: reports.ViewReport, err: nil, }, { desc: "Generate download report with disabled config should succeed", session: authn.Session{ UserID: userID, DomainID: domainID, }, config: validConfig, action: reports.DownloadReport, err: nil, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { _, err := svc.GenerateReport(context.Background(), tc.session, tc.config, tc.action) if tc.err != nil { assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } else { assert.False(t, errors.Contains(err, svcerr.ErrInvalidStatus), fmt.Sprintf("%s: should not get ErrInvalidStatus for instant reports, got %s\n", tc.desc, err)) } }) } }