MG-94 - Add backend support for reports (#107)

* initial implementation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix missing variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix api and add report config to rule engine

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix repo command

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix download request

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix download api

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix add report config

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused parameters

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add limit field to config

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add test and address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add logger

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove logger

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* uncomment code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add status check

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* resolve conflicts

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* rebase code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix startdate when zero

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add time expression parser and logics

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix postgres methods

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix pdf and csv generation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add description for reports

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove aggregation field

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove logs

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix go mod file

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix endpoint and postgres methods

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update report config update methods

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix service test

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unnecessary check

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove endpoints

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix generate PDF and CSV

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* revert UI variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add empty line

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix go mod file

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update download api

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* revert UI variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix download endpoint

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update generateREport method

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing tests

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* refactor generate api

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix csv column

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix csv generator

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove logs

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* updated reports logic and api

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix time conversion

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

---------

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Steve Munene
2025-04-28 10:09:22 +03:00
committed by GitHub
parent 4dd0de64fb
commit 02da121280
35 changed files with 5450 additions and 221 deletions
+28 -22
View File
@@ -472,11 +472,11 @@ type SenMLMessage struct {
Unit string `protobuf:"bytes,3,opt,name=unit,proto3" json:"unit,omitempty"`
Time float64 `protobuf:"fixed64,4,opt,name=time,proto3" json:"time,omitempty"`
UpdateTime float64 `protobuf:"fixed64,5,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"`
Value float64 `protobuf:"fixed64,6,opt,name=value,proto3" json:"value,omitempty"`
StringValue string `protobuf:"bytes,7,opt,name=string_value,json=stringValue,proto3" json:"string_value,omitempty"`
DataValue string `protobuf:"bytes,8,opt,name=data_value,json=dataValue,proto3" json:"data_value,omitempty"`
BoolValue bool `protobuf:"varint,9,opt,name=bool_value,json=boolValue,proto3" json:"bool_value,omitempty"`
Sum float64 `protobuf:"fixed64,10,opt,name=sum,proto3" json:"sum,omitempty"`
Value *float64 `protobuf:"fixed64,6,opt,name=value,proto3,oneof" json:"value,omitempty"`
StringValue *string `protobuf:"bytes,7,opt,name=string_value,json=stringValue,proto3,oneof" json:"string_value,omitempty"`
DataValue *string `protobuf:"bytes,8,opt,name=data_value,json=dataValue,proto3,oneof" json:"data_value,omitempty"`
BoolValue *bool `protobuf:"varint,9,opt,name=bool_value,json=boolValue,proto3,oneof" json:"bool_value,omitempty"`
Sum *float64 `protobuf:"fixed64,10,opt,name=sum,proto3,oneof" json:"sum,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -547,36 +547,36 @@ func (x *SenMLMessage) GetUpdateTime() float64 {
}
func (x *SenMLMessage) GetValue() float64 {
if x != nil {
return x.Value
if x != nil && x.Value != nil {
return *x.Value
}
return 0
}
func (x *SenMLMessage) GetStringValue() string {
if x != nil {
return x.StringValue
if x != nil && x.StringValue != nil {
return *x.StringValue
}
return ""
}
func (x *SenMLMessage) GetDataValue() string {
if x != nil {
return x.DataValue
if x != nil && x.DataValue != nil {
return *x.DataValue
}
return ""
}
func (x *SenMLMessage) GetBoolValue() bool {
if x != nil {
return x.BoolValue
if x != nil && x.BoolValue != nil {
return *x.BoolValue
}
return false
}
func (x *SenMLMessage) GetSum() float64 {
if x != nil {
return x.Sum
if x != nil && x.Sum != nil {
return *x.Sum
}
return 0
}
@@ -742,22 +742,27 @@ const file_readers_v1_readers_proto_rawDesc = "" +
"\achannel\x18\x01 \x01(\tR\achannel\x12\x1a\n" +
"\bsubtopic\x18\x02 \x01(\tR\bsubtopic\x12\x1c\n" +
"\tpublisher\x18\x03 \x01(\tR\tpublisher\x12\x1a\n" +
"\bprotocol\x18\x04 \x01(\tR\bprotocol\"\xa1\x02\n" +
"\bprotocol\x18\x04 \x01(\tR\bprotocol\"\xfb\x02\n" +
"\fSenMLMessage\x12+\n" +
"\x04base\x18\x01 \x01(\v2\x17.readers.v1.BaseMessageR\x04base\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04unit\x18\x03 \x01(\tR\x04unit\x12\x12\n" +
"\x04time\x18\x04 \x01(\x01R\x04time\x12\x1f\n" +
"\vupdate_time\x18\x05 \x01(\x01R\n" +
"updateTime\x12\x14\n" +
"\x05value\x18\x06 \x01(\x01R\x05value\x12!\n" +
"\fstring_value\x18\a \x01(\tR\vstringValue\x12\x1d\n" +
"updateTime\x12\x19\n" +
"\x05value\x18\x06 \x01(\x01H\x00R\x05value\x88\x01\x01\x12&\n" +
"\fstring_value\x18\a \x01(\tH\x01R\vstringValue\x88\x01\x01\x12\"\n" +
"\n" +
"data_value\x18\b \x01(\tR\tdataValue\x12\x1d\n" +
"data_value\x18\b \x01(\tH\x02R\tdataValue\x88\x01\x01\x12\"\n" +
"\n" +
"bool_value\x18\t \x01(\bR\tboolValue\x12\x10\n" +
"bool_value\x18\t \x01(\bH\x03R\tboolValue\x88\x01\x01\x12\x15\n" +
"\x03sum\x18\n" +
" \x01(\x01R\x03sum\"n\n" +
" \x01(\x01H\x04R\x03sum\x88\x01\x01B\b\n" +
"\x06_valueB\x0f\n" +
"\r_string_valueB\r\n" +
"\v_data_valueB\r\n" +
"\v_bool_valueB\x06\n" +
"\x04_sum\"n\n" +
"\vJsonMessage\x12+\n" +
"\x04base\x18\x01 \x01(\v2\x17.readers.v1.BaseMessageR\x04base\x12\x18\n" +
"\acreated\x18\x02 \x01(\x03R\acreated\x12\x18\n" +
@@ -828,6 +833,7 @@ func file_readers_v1_readers_proto_init() {
(*Message_Senml)(nil),
(*Message_Json)(nil),
}
file_readers_v1_readers_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
+69 -15
View File
@@ -15,6 +15,7 @@ import (
chclient "github.com/absmach/callhome/pkg/client"
abrokers "github.com/absmach/magistrala/alarms/brokers"
grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1"
"github.com/absmach/magistrala/consumers/writers/brokers"
"github.com/absmach/magistrala/internal/email"
"github.com/absmach/magistrala/re"
@@ -22,9 +23,13 @@ import (
"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"
"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"
"github.com/absmach/supermq/pkg/messaging"
@@ -40,21 +45,26 @@ import (
)
const (
svcName = "rules_engine"
envPrefixDB = "MG_RE_DB_"
envPrefixHTTP = "MG_RE_HTTP_"
envPrefixAuth = "SMQ_AUTH_GRPC_"
defDB = "r"
defSvcHTTPPort = "9008"
svcName = "rules_engine"
envPrefixDB = "MG_RE_DB_"
envPrefixHTTP = "MG_RE_HTTP_"
envPrefixAuth = "SMQ_AUTH_GRPC_"
defDB = "r"
defSvcHTTPPort = "9008"
envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_"
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
)
type config struct {
LogLevel string `env:"MG_RE_LOG_LEVEL" envDefault:"info"`
InstanceID string `env:"MG_RE_INSTANCE_ID" envDefault:""`
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
LogLevel string `env:"MG_RE_LOG_LEVEL" envDefault:"info"`
InstanceID string `env:"MG_RE_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"`
CacheURL string `env:"MG_RE_CACHE_URL" envDefault:"redis://localhost:6379/0"`
CacheKeyDuration time.Duration `env:"MG_RE_CACHE_KEY_DURATION" envDefault:"10m"`
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
}
func main() {
@@ -177,8 +187,48 @@ func main() {
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
errs := make(chan error)
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)
svc, err := newService(database, errs, msgSub, writersPub, alarmsPub, ec, logger)
regrpcCfg := grpcclient.Config{}
if err := env.ParseWithOptions(&regrpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil {
logger.Error(fmt.Sprintf("failed to load clients gRPC client configuration : %s", err))
exitCode = 1
return
}
client, err := grpcclient.NewHandler(regrpcCfg)
if err != nil {
exitCode = 1
return
}
defer client.Close()
readersClient := grpcClient.NewReadersClient(client.Connection(), regrpcCfg.Timeout)
logger.Info("Readers gRPC client successfully connected to readers gRPC server " + client.Secure())
svc, err := newService(database, errs, msgSub, writersPub, alarmsPub, authz, ec, logger, readersClient)
if err != nil {
logger.Error(fmt.Sprintf("failed to create services: %s", err))
exitCode = 1
@@ -236,7 +286,7 @@ func main() {
}
}
func newService(db pgclient.Database, errs chan error, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, ec email.Config, logger *slog.Logger) (re.Service, error) {
func newService(db pgclient.Database, errs chan error, 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()
@@ -245,7 +295,11 @@ func newService(db pgclient.Database, errs chan error, rePubSub messaging.PubSub
logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error()))
}
csvc := re.NewService(repo, errs, idp, rePubSub, writersPub, alarmsPub, re.NewTicker(time.Minute), emailerClient)
csvc := re.NewService(repo, errs, idp, rePubSub, writersPub, alarmsPub, re.NewTicker(time.Minute), emailerClient, readersClient)
csvc, err = middleware.AuthorizationMiddleware(csvc, authz)
if err != nil {
return nil, err
}
csvc = middleware.LoggingMiddleware(csvc, logger)
return csvc, nil
+1 -1
View File
@@ -36,5 +36,5 @@ func (n *notifier) Notify(from string, to []string, msg *messaging.Message) erro
values := string(msg.GetPayload())
content := fmt.Sprintf(contentTemplate, msg.GetPublisher(), msg.GetProtocol(), values)
return n.agent.Send(to, from, subject, "", "", content, footer)
return n.agent.Send(to, from, subject, "", "", content, footer, map[string][]byte{})
}
+10
View File
@@ -192,6 +192,16 @@ services:
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_CLIENT_CA_CERTS: ${MG_TIMESCALE_READER_GRPC_CLIENT_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_RE_HTTP_PORT}:${MG_RE_HTTP_PORT}
networks:
+9 -2
View File
@@ -23,6 +23,8 @@ require (
github.com/jackc/pgtype v1.14.4
github.com/jackc/pgx/v5 v5.7.4
github.com/jmoiron/sqlx v1.4.0
github.com/johnfercher/maroto v1.0.0
github.com/lib/pq v1.10.9
github.com/ory/dockertest/v3 v3.12.0
github.com/pelletier/go-toml v1.9.5
github.com/prometheus/client_golang v1.22.0
@@ -43,6 +45,12 @@ require (
moul.io/http2curl v1.0.0
)
require (
github.com/boombuler/barcode v1.0.1 // indirect
github.com/jung-kurt/gofpdf v1.16.2 // indirect
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 // indirect
)
require (
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
@@ -50,7 +58,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/absmach/certs v0.0.0-20250303232207-ef00d309ca02 // indirect
github.com/absmach/senml v1.0.7 // indirect
github.com/absmach/senml v1.0.7
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
@@ -85,7 +93,6 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jzelinskie/stringz v0.0.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
+16
View File
@@ -50,6 +50,9 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -185,6 +188,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
@@ -264,6 +268,8 @@ github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/johnfercher/maroto v1.0.0 h1:yo26a/Mxj2YbHCzpIW7FypKtdvv9BdeLNHaApHwLCXU=
github.com/johnfercher/maroto v1.0.0/go.mod h1:qeujdhKT+677jMjGWlIa5OCgR04GgIHvByJ6pSC+hOw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -272,6 +278,9 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/jzelinskie/stringz v0.0.3 h1:0GhG3lVMYrYtIvRbxvQI6zqRTT1P1xyQlpa0FhfUXas=
github.com/jzelinskie/stringz v0.0.3/go.mod h1:hHYbgxJuNLRw91CmpuFsYEOyQqpDVFg8pvEh23vy4P0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -367,6 +376,9 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -415,6 +427,9 @@ github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2N
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
@@ -552,6 +567,7 @@ golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbV
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+19 -1
View File
@@ -5,6 +5,8 @@ package email
import (
"bytes"
"fmt"
"io"
"net/mail"
"strconv"
"strings"
@@ -71,7 +73,7 @@ func New(c *Config) (*Agent, error) {
}
// Send sends e-mail.
func (a *Agent) Send(to []string, from, subject, header, user, content, footer string) error {
func (a *Agent) Send(to []string, from, subject, header, user, content, footer string, attachments map[string][]byte) error {
if a.tmpl == nil {
return errMissingEmailTemplate
}
@@ -102,6 +104,22 @@ func (a *Agent) Send(to []string, from, subject, header, user, content, footer s
m.SetHeader("Subject", subject)
m.SetBody("text/plain", buff.String())
for filename, data := range attachments {
reader := bytes.NewReader(data)
settings := []gomail.FileSetting{
gomail.SetHeader(map[string][]string{
"Content-Disposition": {fmt.Sprintf(`attachment; filename="%s"`, filename)},
}),
gomail.SetCopyFunc(func(w io.Writer) error {
_, err := io.Copy(w, reader)
return err
}),
}
m.Attach(filename, settings...)
}
if err := a.dial.DialAndSend(m); err != nil {
return errors.Wrap(errSendMail, err)
}
+5 -5
View File
@@ -60,11 +60,11 @@ message SenMLMessage {
string unit = 3;
double time = 4;
double update_time = 5;
double value = 6;
string string_value = 7;
string data_value = 8;
bool bool_value = 9;
double sum = 10;
optional double value = 6;
optional string string_value = 7;
optional string data_value = 8;
optional bool bool_value = 9;
optional double sum = 10;
}
message JsonMessage {
+86
View File
@@ -0,0 +1,86 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reltime
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/absmach/magistrala/pkg/errors"
)
var (
re = regexp.MustCompile(`(?i)^now\(\)([\+\-])(.+)$`)
ErrInvalidDuration = errors.New("invalid duration format")
ErrInvalidExpression = errors.New("invalid time expression")
ErrUnsupportedUnit = errors.New("unsupported unit")
)
func Parse(expr string) (time.Time, error) {
now := time.Now()
expr = strings.ReplaceAll(expr, " ", "")
if strings.EqualFold(expr, "now()") {
return now, nil
}
matches := re.FindStringSubmatch(expr)
if len(matches) != 3 {
return time.Time{}, errors.Wrap(ErrInvalidExpression, fmt.Errorf("%s", expr))
}
sign := matches[1]
durStr := matches[2]
if strings.ContainsAny(durStr, "+-") {
return time.Time{}, errors.Wrap(ErrInvalidExpression, fmt.Errorf("%s", expr))
}
dur, err := parseComplexDuration(durStr)
if err != nil {
return time.Time{}, err
}
if sign == "-" {
return now.Add(-dur), nil
}
return now.Add(dur), nil
}
func parseComplexDuration(s string) (time.Duration, error) {
var total time.Duration
re := regexp.MustCompile(`(\d+)([smhdwMY])`)
matches := re.FindAllStringSubmatch(s, -1)
if matches == nil {
return 0, errors.Wrap(ErrInvalidDuration, fmt.Errorf("%s", s))
}
for _, match := range matches {
val, _ := strconv.Atoi(match[1])
unit := match[2]
var d time.Duration
switch unit {
case "s":
d = time.Duration(val) * time.Second
case "m":
d = time.Duration(val) * time.Minute
case "h":
d = time.Duration(val) * time.Hour
case "d":
d = time.Duration(val) * 24 * time.Hour
case "w":
d = time.Duration(val) * 7 * 24 * time.Hour
default:
return 0, errors.Wrap(ErrUnsupportedUnit, fmt.Errorf("%s", unit))
}
total += d
}
return total, nil
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reltime
import (
"fmt"
"testing"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
now := time.Now()
tests := []struct {
desc string
expr string
expected time.Time
err error
}{
{
desc: "testing expression now()-5d",
expr: "now()-5d",
expected: now.Add(-5 * 24 * time.Hour),
err: nil,
},
{
desc: "testing expression now()+2h30m",
expr: "now()+2h30m",
expected: now.Add(2*time.Hour + 30*time.Minute),
err: nil,
},
{
desc: "testing expression now()-1w3d10h40m",
expr: "now()-1w3d10h40m",
expected: now.Add(-(7*24+3*24+10)*time.Hour - 40*time.Minute),
err: nil,
},
{
desc: "testing expression yesterday",
expr: "yesterday",
err: ErrInvalidExpression,
},
{
desc: "testing expression now()--5d",
expr: "now()--5d",
err: ErrInvalidExpression,
},
{
desc: "testing expression now()+",
expr: "now()+",
err: ErrInvalidExpression,
},
{
desc: "testing expression now()+5r",
expr: "now()+5r",
err: ErrInvalidDuration,
},
{
desc: "testing expression now()+5M",
expr: "now()+5M",
err: ErrUnsupportedUnit,
},
}
for _, tc := range tests {
got, err := Parse(tc.expr)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v and response time %v\n", tc.desc, tc.err, err, got))
if err == nil {
assert.WithinDuration(t, tc.expected, got, time.Duration(10*time.Second))
}
}
}
+224
View File
@@ -177,3 +177,227 @@ 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
}
}
+706
View File
@@ -55,6 +55,29 @@ var (
"name": "test",
},
}
reportConfig = re.ReportConfig{
ID: validID,
Name: namegen.Generate(),
DomainID: domainID,
Schedule: schedule,
Status: re.EnabledStatus,
Metrics: []re.Metric{
{
ChannelID: "channel1",
ClientID: "channel2",
Name: "metric_name",
},
},
Config: &re.MetricConfig{
From: "now()-1h",
To: "now()",
Aggregation: re.AggConfig{AggType: re.AggregationAVG, Interval: "1h"},
},
Email: &re.EmailSetting{
To: []string{"test@example.com"},
Subject: "Test Report",
},
}
)
type testRequest struct {
@@ -966,3 +989,686 @@ 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()
})
}
}
+150
View File
@@ -4,14 +4,28 @@
package api
import (
"fmt"
"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")
)
const (
maxLimitSize = 1000
MaxNameSize = 1024
errInvalidMetric = "invalid metric[%d]: %w"
)
type addRuleReq struct {
@@ -103,3 +117,139 @@ 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 {
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
}
+147
View File
@@ -6,6 +6,7 @@ package api
import (
"fmt"
"net/http"
"time"
"github.com/absmach/magistrala/re"
"github.com/absmach/supermq"
@@ -18,6 +19,11 @@ 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 {
@@ -136,3 +142,144 @@ 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
}
+258 -53
View File
@@ -6,6 +6,7 @@ package api
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
@@ -23,10 +24,13 @@ import (
)
const (
idKey = "ruleID"
ruleIdKey = "ruleID"
reportIdKey = "reportID"
inputChannelKey = "input_channel"
outputChannelKey = "output_channel"
statusKey = "status"
actionKey = "action"
defAction = "view"
)
// MakeHandler creates an HTTP handler for the service endpoints.
@@ -36,63 +40,131 @@ func MakeHandler(svc re.Service, authn mgauthn.Authentication, mux *chi.Mux, log
}
mux.Group(func(r chi.Router) {
r.Use(api.AuthenticateMiddleware(authn, true))
r.Route("/{domainID}/rules", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addRuleEndpoint(svc),
decodeAddRuleRequest,
api.EncodeResponse,
opts...,
), "create_rule").ServeHTTP)
r.Route("/{domainID}", func(r chi.Router) {
r.Route("/rules", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addRuleEndpoint(svc),
decodeAddRuleRequest,
api.EncodeResponse,
opts...,
), "create_rule").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listRulesEndpoint(svc),
decodeListRulesRequest,
api.EncodeResponse,
opts...,
), "list_rules").ServeHTTP)
r.Route("/{ruleID}", func(r chi.Router) {
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
viewRuleEndpoint(svc),
decodeViewRuleRequest,
listRulesEndpoint(svc),
decodeListRulesRequest,
api.EncodeResponse,
opts...,
), "view_rule").ServeHTTP)
), "list_rules").ServeHTTP)
r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
updateRuleEndpoint(svc),
decodeUpdateRuleRequest,
api.EncodeResponse,
opts...,
), "update_rule").ServeHTTP)
r.Route("/{ruleID}", func(r chi.Router) {
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
viewRuleEndpoint(svc),
decodeViewRuleRequest,
api.EncodeResponse,
opts...,
), "view_rule").ServeHTTP)
r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer(
updateRuleScheduleEndpoint(svc),
decodeUpdateRuleScheduleRequest,
api.EncodeResponse,
opts...,
), "update_rule_scheduler").ServeHTTP)
r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
updateRuleEndpoint(svc),
decodeUpdateRuleRequest,
api.EncodeResponse,
opts...,
), "update_rule").ServeHTTP)
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
deleteRuleEndpoint(svc),
decodeDeleteRuleRequest,
api.EncodeResponse,
opts...,
), "delete_rule").ServeHTTP)
r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer(
updateRuleScheduleEndpoint(svc),
decodeUpdateRuleScheduleRequest,
api.EncodeResponse,
opts...,
), "update_rule_scheduler").ServeHTTP)
r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
enableRuleEndpoint(svc),
decodeUpdateRuleStatusRequest,
api.EncodeResponse,
opts...,
), "enable_rule").ServeHTTP)
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
deleteRuleEndpoint(svc),
decodeDeleteRuleRequest,
api.EncodeResponse,
opts...,
), "delete_rule").ServeHTTP)
r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
disableRuleEndpoint(svc),
decodeUpdateRuleStatusRequest,
api.EncodeResponse,
r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
enableRuleEndpoint(svc),
decodeUpdateRuleStatusRequest,
api.EncodeResponse,
opts...,
), "enable_rule").ServeHTTP)
r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
disableRuleEndpoint(svc),
decodeUpdateRuleStatusRequest,
api.EncodeResponse,
opts...,
), "disable_rule").ServeHTTP)
})
})
r.Route("/reports", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
generateReportEndpoint(svc),
decodeGenerateReportRequest,
encodeFileDownloadResponse,
opts...,
), "disable_rule").ServeHTTP)
), "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)
})
})
})
})
@@ -115,7 +187,7 @@ func decodeAddRuleRequest(_ context.Context, r *http.Request) (interface{}, erro
}
func decodeViewRuleRequest(_ context.Context, r *http.Request) (interface{}, error) {
id := chi.URLParam(r, idKey)
id := chi.URLParam(r, ruleIdKey)
return viewRuleReq{id: id}, nil
}
@@ -127,7 +199,7 @@ func decodeUpdateRuleRequest(_ context.Context, r *http.Request) (interface{}, e
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
}
rule.ID = chi.URLParam(r, idKey)
rule.ID = chi.URLParam(r, ruleIdKey)
return updateRuleReq{Rule: rule}, nil
}
@@ -137,7 +209,7 @@ func decodeUpdateRuleScheduleRequest(_ context.Context, r *http.Request) (interf
}
req := updateRuleScheduleReq{
id: chi.URLParam(r, idKey),
id: chi.URLParam(r, ruleIdKey),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
@@ -148,7 +220,7 @@ func decodeUpdateRuleScheduleRequest(_ context.Context, r *http.Request) (interf
func decodeUpdateRuleStatusRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := updateRuleStatusReq{
id: chi.URLParam(r, idKey),
id: chi.URLParam(r, ruleIdKey),
}
return req, nil
}
@@ -195,7 +267,140 @@ func decodeListRulesRequest(_ context.Context, r *http.Request) (interface{}, er
}
func decodeDeleteRuleRequest(_ context.Context, r *http.Request) (interface{}, error) {
id := chi.URLParam(r, idKey)
id := chi.URLParam(r, ruleIdKey)
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)
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ func (re *re) sendEmail(l *lua.LState) int {
}
})
if err := re.email.SendEmailNotification(recipients, "", subject, "", "", content, ""); err != nil {
if err := re.email.SendEmailNotification(recipients, "", subject, "", "", content, "", make(map[string][]byte)); err != nil {
return 0
}
return 1
+1 -2
View File
@@ -3,8 +3,7 @@
package re
//go:generate mockery --name Emailer --output=./mocks --filename emailer.go --quiet --note "Copyright (c) Abstract Machines"
type Emailer interface {
// SendEmailNotification sends an email to the recipients based on a trigger.
SendEmailNotification(to []string, from, subject, header, user, content, footer string) error
SendEmailNotification(to []string, from, subject, header, user, content, footer string, attachments map[string][]byte) error
}
+2 -2
View File
@@ -19,6 +19,6 @@ func New(a *email.Config) (re.Emailer, error) {
return &emailer{agent: e}, err
}
func (e *emailer) SendEmailNotification(to []string, from, subject, header, user, content, footer string) error {
return e.agent.Send(to, from, subject, header, user, content, footer)
func (e *emailer) SendEmailNotification(to []string, from, subject, header, user, content, footer string, attachments map[string][]byte) error {
return e.agent.Send(to, from, subject, header, user, content, footer, attachments)
}
+402
View File
@@ -0,0 +1,402 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package re
import (
"bytes"
"encoding/csv"
"fmt"
"sort"
"time"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/transformers/senml"
"github.com/johnfercher/maroto/pkg/color"
"github.com/johnfercher/maroto/pkg/consts"
"github.com/johnfercher/maroto/pkg/pdf"
"github.com/johnfercher/maroto/pkg/props"
)
func generatePDFReport(reports []Report) ([]byte, error) {
m := pdf.NewMaroto(consts.Portrait, consts.A4)
m.SetPageMargins(10, 15, 10)
primaryColor := color.Color{Red: 41, Green: 128, Blue: 185} // Blue
secondaryColor := color.Color{Red: 26, Green: 82, Blue: 118} // Darker blue
subtleColor := color.Color{Red: 189, Green: 195, Blue: 199} // Light gray
tableHeaderBg := color.Color{Red: 236, Green: 240, Blue: 241} // Very light gray
alternateRow := color.Color{Red: 245, Green: 247, Blue: 249} // Even lighter gray
textPrimary := color.Color{Red: 44, Green: 62, Blue: 80} // Dark blue-gray
textSecondary := color.Color{Red: 127, Green: 140, Blue: 141} // Medium gray
white := color.NewWhite()
m.RegisterHeader(func() {
m.SetBackgroundColor(primaryColor)
m.Row(2, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(20, func() {
m.Col(2, func() {})
m.Col(8, func() {
m.Text("Magistrala IoT Report", props.Text{
Size: 20,
Style: consts.Bold,
Color: primaryColor,
Align: consts.Center,
Top: 6,
})
})
m.Col(2, func() {
m.Text(time.Now().Format("02 Jan 2006"), props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Right,
Color: textSecondary,
Top: 8,
})
})
})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(0.25, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.25, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(5, func() {})
})
m.RegisterFooter(func() {
currentPage := m.GetCurrentPage()
m.Row(5, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.25, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(0.25, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(4, func() {
m.Text("Generated: "+time.Now().Format("15:04:05"), props.Text{
Size: 8,
Style: consts.Italic,
Align: consts.Left,
Color: textSecondary,
Top: 3,
})
})
m.Col(4, func() {
m.Text(fmt.Sprintf("Page %d", currentPage+1), props.Text{
Size: 9,
Style: consts.Bold,
Align: consts.Center,
Color: textPrimary,
Top: 3,
})
})
m.Col(4, func() {
m.Text("Magistrala System", props.Text{
Size: 8,
Style: consts.Italic,
Align: consts.Right,
Color: textSecondary,
Top: 3,
})
})
})
})
headers := []string{"Time", "Name", "Value", "Unit", "Subtopic"}
widths := []uint{4, 2, 2, 2, 2}
for i, report := range reports {
if i > 0 {
m.AddPage()
}
m.Row(0.5, func() {
m.Col(1, func() {})
})
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(12, func() {
m.Text("Metrics", props.Text{
Size: 16,
Style: consts.Bold,
Color: secondaryColor,
Top: 2,
})
})
})
m.SetBackgroundColor(alternateRow)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.Row(8, func() {
m.Col(2, func() {
m.Text("Device ID: ", props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Left,
Color: textPrimary,
Top: 1,
})
})
m.Col(10, func() {
m.Text(report.Metric.ClientID, props.Text{
Size: 11,
Style: consts.Italic,
Color: textPrimary,
Top: 1,
})
})
})
m.Row(8, func() {
m.Col(2, func() {
m.Text("Channel ID: ", props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Left,
Color: textPrimary,
Top: 1,
})
})
m.Col(10, func() {
m.Text(report.Metric.ChannelID, props.Text{
Size: 11,
Style: consts.Italic,
Color: textPrimary,
Top: 1,
})
})
})
m.SetBackgroundColor(alternateRow)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(12, func() {
m.Text(fmt.Sprintf("Total Records: %d", len(report.Messages)), props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Right,
Color: textSecondary,
Top: 2,
})
})
})
m.SetBackgroundColor(primaryColor)
m.Row(1, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(tableHeaderBg)
m.Row(10, func() {
for i, header := range headers {
m.Col(widths[i], func() {
m.Text(header, props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Center,
Top: 2,
Color: secondaryColor,
})
})
}
})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
useAlternateColor := false
for _, msg := range report.Messages {
if useAlternateColor {
m.SetBackgroundColor(alternateRow)
}
m.Row(9, func() {
m.Col(widths[0], func() {
m.Text(formatTime(msg.Time), props.Text{
Size: 10,
Align: consts.Center,
Top: 2,
Color: textPrimary,
})
})
m.Col(widths[1], func() {
m.Text(msg.Name, props.Text{
Size: 10,
Style: consts.Bold,
Align: consts.Center,
Top: 2,
Color: primaryColor,
})
})
m.Col(widths[2], func() {
m.Text(formatValue(msg), props.Text{
Size: 10,
Style: consts.Normal,
Align: consts.Center,
Top: 2,
Color: textPrimary,
})
})
m.Col(widths[3], func() {
m.Text(msg.Unit, props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Center,
Top: 2,
Color: textSecondary,
})
})
m.Col(widths[4], func() {
m.Text(msg.Subtopic, props.Text{
Size: 10,
Align: consts.Center,
Top: 2,
Color: secondaryColor,
})
})
})
if !useAlternateColor {
m.Row(0.2, func() {
m.Col(12, func() {})
})
}
useAlternateColor = !useAlternateColor
m.SetBackgroundColor(white)
}
}
buf, err := m.Output()
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return buf.Bytes(), nil
}
func formatTime(t float64) string {
if t > 9999999999 {
return time.Unix(0, int64(t)).Format("2006-01-02 15:04:05")
}
return time.Unix(int64(t), 0).Format("2006-01-02 15:04:05")
}
func formatValue(msg senml.Message) string {
switch {
case msg.Value != nil:
return fmt.Sprintf("%.2f", *msg.Value)
case msg.StringValue != nil:
return *msg.StringValue
case msg.BoolValue != nil:
return fmt.Sprintf("%t", *msg.BoolValue)
case msg.DataValue != nil:
return *msg.DataValue
default:
return "N/A"
}
}
func generateCSVReport(reports []Report) ([]byte, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
headers := []string{"Time", "Name", "Subtopic", "Value", "Unit"}
for i, report := range reports {
if len(report.Messages) == 0 {
continue
}
if i > 0 {
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
if err := writer.Write([]string{"Report Information:"}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write(headers); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
sort.Slice(report.Messages, func(i, j int) bool {
return report.Messages[i].Time < report.Messages[j].Time
})
for _, msg := range report.Messages {
timeStr := formatTime(msg.Time)
var valueStr string
if msg.Value != nil {
valueStr = fmt.Sprintf("%.2f", *msg.Value)
} else if msg.StringValue != nil {
valueStr = *msg.StringValue
} else if msg.BoolValue != nil {
valueStr = fmt.Sprintf("%v", *msg.BoolValue)
} else if msg.DataValue != nil {
valueStr = *msg.DataValue
} else {
valueStr = "N/A"
}
row := []string{
timeStr,
msg.Name,
msg.Subtopic,
valueStr,
msg.Unit,
}
if err := writer.Write(row); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
}
writer.Flush()
if err := writer.Error(); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return buf.Bytes(), nil
}
+48 -14
View File
@@ -35,11 +35,25 @@ func (re *re) Handle(msg *messaging.Message) error {
return err
}
reportConfigs, err := re.repo.ListReportsConfig(ctx, pm)
if err != nil {
return err
}
for _, r := range page.Rules {
go func(ctx context.Context) {
re.errors <- re.process(ctx, r, msg)
}(ctx)
}
for _, cfg := range reportConfigs.ReportConfigs {
go func(ctx context.Context) {
if err := re.processReportConfig(ctx, cfg); err != nil {
re.errors <- err
}
}(ctx)
}
return nil
}
@@ -83,6 +97,13 @@ func (re *re) process(ctx context.Context, r Rule, msg *messaging.Message) error
return err
}
func (re *re) processReportConfig(ctx context.Context, cfg ReportConfig) error {
if _, err := re.generateReport(ctx, cfg, EmailReport); err != nil {
return err
}
return nil
}
func (re *re) handleOutput(ctx context.Context, o ScriptOutput, r Rule, msg *messaging.Message, val interface{}) error {
switch o {
case Channels:
@@ -119,7 +140,7 @@ func (re *re) StartScheduler(ctx context.Context) error {
}
for _, rule := range page.Rules {
if rule.shouldRun(startTime) {
if rule.Schedule.ShouldRun(startTime) {
go func(r Rule) {
msg := &messaging.Message{
Channel: r.InputChannel,
@@ -129,48 +150,61 @@ func (re *re) StartScheduler(ctx context.Context) error {
}(rule)
}
}
reportConfigs, err := re.repo.ListReportsConfig(ctx, pm)
if err != nil {
return err
}
for _, cfg := range reportConfigs.ReportConfigs {
if cfg.Schedule.ShouldRun(startTime) {
go func(config ReportConfig) {
re.errors <- re.processReportConfig(ctx, config)
}(cfg)
}
}
}
}
}
func (r Rule) shouldRun(startTime time.Time) bool {
func (s Schedule) ShouldRun(startTime time.Time) bool {
// Don't run if the rule's start time is in the future
// This allows scheduling rules to start at a specific future time
if r.Schedule.StartDateTime.After(startTime) {
if s.StartDateTime.After(startTime) {
return false
}
t := r.Schedule.Time.Truncate(time.Minute).UTC()
t := s.Time.Truncate(time.Minute).UTC()
startTimeOnly := time.Date(0, 1, 1, startTime.Hour(), startTime.Minute(), 0, 0, time.UTC)
if t.Equal(startTimeOnly) {
return true
}
if r.Schedule.RecurringPeriod == 0 {
if s.RecurringPeriod == 0 {
return false
}
period := int(r.Schedule.RecurringPeriod)
period := int(s.RecurringPeriod)
switch r.Schedule.Recurring {
switch s.Recurring {
case Daily:
if r.Schedule.RecurringPeriod > 0 {
daysSinceStart := startTime.Sub(r.Schedule.StartDateTime).Hours() / hoursInDay
if s.RecurringPeriod > 0 {
daysSinceStart := startTime.Sub(s.StartDateTime).Hours() / hoursInDay
if int(daysSinceStart)%period == 0 {
return true
}
}
case Weekly:
if r.Schedule.RecurringPeriod > 0 {
weeksSinceStart := startTime.Sub(r.Schedule.StartDateTime).Hours() / (hoursInDay * daysInWeek)
if s.RecurringPeriod > 0 {
weeksSinceStart := startTime.Sub(s.StartDateTime).Hours() / (hoursInDay * daysInWeek)
if int(weeksSinceStart)%period == 0 {
return true
}
}
case Monthly:
if r.Schedule.RecurringPeriod > 0 {
monthsSinceStart := (startTime.Year()-r.Schedule.StartDateTime.Year())*monthsInYear +
int(startTime.Month()-r.Schedule.StartDateTime.Month())
if s.RecurringPeriod > 0 {
monthsSinceStart := (startTime.Year()-s.StartDateTime.Year())*monthsInYear +
int(startTime.Month()-s.StartDateTime.Month())
if monthsSinceStart%period == 0 {
return true
}
+331
View File
@@ -0,0 +1,331 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"github.com/absmach/magistrala/re"
"github.com/absmach/supermq/pkg/authn"
smqauthz "github.com/absmach/supermq/pkg/authz"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/messaging"
"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")
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")
)
type authorizationMiddleware struct {
svc re.Service
authz smqauthz.Authorization
}
// AuthorizationMiddleware adds authorization to the re service.
func AuthorizationMiddleware(svc re.Service, authz smqauthz.Authorization) (re.Service, error) {
return &authorizationMiddleware{
svc: svc,
authz: authz,
}, nil
}
func (am *authorizationMiddleware) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, 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.Rule{}, errors.Wrap(errDomainCreateRules, err)
}
return am.svc.AddRule(ctx, session, r)
}
func (am *authorizationMiddleware) ViewRule(ctx context.Context, session authn.Session, id string) (re.Rule, 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.Rule{}, errors.Wrap(errDomainViewRules, err)
}
return am.svc.ViewRule(ctx, session, id)
}
func (am *authorizationMiddleware) UpdateRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, 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.Rule{}, errors.Wrap(errDomainUpdateRules, err)
}
return am.svc.UpdateRule(ctx, session, r)
}
func (am *authorizationMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, 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.Rule{}, errors.Wrap(errDomainUpdateRules, err)
}
return am.svc.UpdateRuleSchedule(ctx, session, r)
}
func (am *authorizationMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) {
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.Page{}, errors.Wrap(errDomainViewRules, err)
}
return am.svc.ListRules(ctx, session, pm)
}
func (am *authorizationMiddleware) RemoveRule(ctx context.Context, session authn.Session, id string) error {
if err := am.authorize(ctx, 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(errDomainDeleteRules, err)
}
return am.svc.RemoveRule(ctx, session, id)
}
func (am *authorizationMiddleware) EnableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) {
if err := am.authorize(ctx, 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.Rule{}, errors.Wrap(errDomainUpdateRules, err)
}
return am.svc.EnableRule(ctx, session, id)
}
func (am *authorizationMiddleware) DisableRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) {
if err := am.authorize(ctx, 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.Rule{}, errors.Wrap(errDomainUpdateRules, err)
}
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)
}
func (am *authorizationMiddleware) Handle(msg *messaging.Message) error {
return am.svc.Handle(msg)
}
func (am *authorizationMiddleware) Cancel() error {
return am.svc.Cancel()
}
func (am *authorizationMiddleware) authorize(ctx context.Context, pr smqauthz.PolicyReq) error {
if err := am.authz.Authorize(ctx, pr); err != nil {
return err
}
return nil
}
+171
View File
@@ -220,3 +220,174 @@ 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)
}
+73 -26
View File
@@ -1,33 +1,15 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// 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 mock "github.com/stretchr/testify/mock"
// Emailer is an autogenerated mock type for the Emailer type
type Emailer struct {
mock.Mock
}
// SendEmailNotification provides a mock function with given fields: to, from, subject, header, user, content, footer
func (_m *Emailer) SendEmailNotification(to []string, from string, subject string, header string, user string, content string, footer string) error {
ret := _m.Called(to, from, subject, header, user, content, footer)
if len(ret) == 0 {
panic("no return value specified for SendEmailNotification")
}
var r0 error
if rf, ok := ret.Get(0).(func([]string, string, string, string, string, string, string) error); ok {
r0 = rf(to, from, subject, header, user, content, footer)
} else {
r0 = ret.Error(0)
}
return r0
}
import (
mock "github.com/stretchr/testify/mock"
)
// NewEmailer creates a new instance of Emailer. 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.
@@ -42,3 +24,68 @@ func NewEmailer(t interface {
return mock
}
// Emailer is an autogenerated mock type for the Emailer type
type Emailer struct {
mock.Mock
}
type Emailer_Expecter struct {
mock *mock.Mock
}
func (_m *Emailer) EXPECT() *Emailer_Expecter {
return &Emailer_Expecter{mock: &_m.Mock}
}
// SendEmailNotification provides a mock function for the type Emailer
func (_mock *Emailer) SendEmailNotification(to []string, from string, subject string, header string, user string, content string, footer string, attachments map[string][]byte) error {
ret := _mock.Called(to, from, subject, header, user, content, footer, attachments)
if len(ret) == 0 {
panic("no return value specified for SendEmailNotification")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func([]string, string, string, string, string, string, string, map[string][]byte) error); ok {
r0 = returnFunc(to, from, subject, header, user, content, footer, attachments)
} else {
r0 = ret.Error(0)
}
return r0
}
// Emailer_SendEmailNotification_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmailNotification'
type Emailer_SendEmailNotification_Call struct {
*mock.Call
}
// SendEmailNotification is a helper method to define mock.On call
// - to
// - from
// - subject
// - header
// - user
// - content
// - footer
// - attachments
func (_e *Emailer_Expecter) SendEmailNotification(to interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}, attachments interface{}) *Emailer_SendEmailNotification_Call {
return &Emailer_SendEmailNotification_Call{Call: _e.mock.On("SendEmailNotification", to, from, subject, header, user, content, footer, attachments)}
}
func (_c *Emailer_SendEmailNotification_Call) Run(run func(to []string, from string, subject string, header string, user string, content string, footer string, attachments map[string][]byte)) *Emailer_SendEmailNotification_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]string), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].(map[string][]byte))
})
return _c
}
func (_c *Emailer_SendEmailNotification_Call) Return(err error) *Emailer_SendEmailNotification_Call {
_c.Call.Return(err)
return _c
}
func (_c *Emailer_SendEmailNotification_Call) RunAndReturn(run func(to []string, from string, subject string, header string, user string, content string, footer string, attachments map[string][]byte) error) *Emailer_SendEmailNotification_Call {
_c.Call.Return(run)
return _c
}
+390 -15
View File
@@ -41,6 +41,61 @@ 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)
@@ -96,6 +151,61 @@ 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)
@@ -151,6 +261,52 @@ 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)
@@ -197,6 +353,171 @@ 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
}
// 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)
@@ -308,8 +629,8 @@ func (_c *Repository_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.
}
// UpdateRuleStatus provides a mock function for the type Repository
func (_mock *Repository) UpdateRuleStatus(ctx context.Context, id string, status re.Status) (re.Rule, error) {
ret := _mock.Called(ctx, id, status)
func (_mock *Repository) UpdateRuleStatus(ctx context.Context, r re.Rule) (re.Rule, error) {
ret := _mock.Called(ctx, r)
if len(ret) == 0 {
panic("no return value specified for UpdateRuleStatus")
@@ -317,16 +638,16 @@ func (_mock *Repository) UpdateRuleStatus(ctx context.Context, id string, status
var r0 re.Rule
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, re.Status) (re.Rule, error)); ok {
return returnFunc(ctx, id, status)
if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok {
return returnFunc(ctx, r)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, re.Status) re.Rule); ok {
r0 = returnFunc(ctx, id, status)
if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok {
r0 = returnFunc(ctx, r)
} else {
r0 = ret.Get(0).(re.Rule)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, re.Status) error); ok {
r1 = returnFunc(ctx, id, status)
if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok {
r1 = returnFunc(ctx, r)
} else {
r1 = ret.Error(1)
}
@@ -340,15 +661,14 @@ type Repository_UpdateRuleStatus_Call struct {
// UpdateRuleStatus is a helper method to define mock.On call
// - ctx
// - id
// - status
func (_e *Repository_Expecter) UpdateRuleStatus(ctx interface{}, id interface{}, status interface{}) *Repository_UpdateRuleStatus_Call {
return &Repository_UpdateRuleStatus_Call{Call: _e.mock.On("UpdateRuleStatus", ctx, id, status)}
// - r
func (_e *Repository_Expecter) UpdateRuleStatus(ctx interface{}, r interface{}) *Repository_UpdateRuleStatus_Call {
return &Repository_UpdateRuleStatus_Call{Call: _e.mock.On("UpdateRuleStatus", ctx, r)}
}
func (_c *Repository_UpdateRuleStatus_Call) Run(run func(ctx context.Context, id string, status re.Status)) *Repository_UpdateRuleStatus_Call {
func (_c *Repository_UpdateRuleStatus_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRuleStatus_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(re.Status))
run(args[0].(context.Context), args[1].(re.Rule))
})
return _c
}
@@ -358,7 +678,62 @@ func (_c *Repository_UpdateRuleStatus_Call) Return(rule re.Rule, err error) *Rep
return _c
}
func (_c *Repository_UpdateRuleStatus_Call) RunAndReturn(run func(ctx context.Context, id string, status re.Status) (re.Rule, error)) *Repository_UpdateRuleStatus_Call {
func (_c *Repository_UpdateRuleStatus_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRuleStatus_Call {
_c.Call.Return(run)
return _c
}
// 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
}
+496
View File
@@ -43,6 +43,62 @@ 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)
@@ -143,6 +199,62 @@ 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)
@@ -199,6 +311,62 @@ 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)
@@ -255,6 +423,63 @@ 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)
@@ -300,6 +525,62 @@ 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)
@@ -356,6 +637,53 @@ 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)
@@ -448,6 +776,118 @@ 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)
@@ -560,6 +1000,62 @@ 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)
+26
View File
@@ -43,6 +43,32 @@ 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;`,
},
},
},
}
}
+305 -22
View File
@@ -33,7 +33,7 @@ func (repo *PostgresRepository) AddRule(ctx context.Context, r re.Rule) (re.Rule
`
dbr, err := ruleToDb(r)
if err != nil {
return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err)
return re.Rule{}, err
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
@@ -79,30 +79,39 @@ func (repo *PostgresRepository) ViewRule(ctx context.Context, id string) (re.Rul
return ret, nil
}
func (repo *PostgresRepository) UpdateRuleStatus(ctx context.Context, id string, status re.Status) (re.Rule, error) {
q := `
UPDATE rules
SET status = $2
WHERE id = $1
RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_output, logic_value,
output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
`
row := repo.DB.QueryRowxContext(ctx, q, id, status)
if err := row.Err(); err != nil {
return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
func (repo *PostgresRepository) UpdateRuleStatus(ctx context.Context, r re.Rule) (re.Rule, error) {
q := `UPDATE rules
SET status = :status, updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id
RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_output, logic_value,
output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;`
var dbr dbRule
if err := row.StructScan(&dbr); err != nil {
return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
rule, err := dbToRule(dbr)
dbr, err := ruleToDb(r)
if err != nil {
return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return rule, nil
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
var res dbRule
if row.Next() {
if err := row.StructScan(&res); err != nil {
return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
rule, err := dbToRule(res)
if err != nil {
return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return rule, nil
}
return re.Rule{}, repoerr.ErrNotFound
}
func (repo *PostgresRepository) UpdateRule(ctx context.Context, r re.Rule) (re.Rule, error) {
@@ -229,7 +238,7 @@ func (repo *PostgresRepository) ListRules(ctx context.Context, pm re.PageMeta) (
if pm.Offset != 0 {
pgData += " OFFSET :offset"
}
pq := pageQuery(pm)
pq := pageRulesQuery(pm)
q := fmt.Sprintf(`
SELECT id, name, domain_id, input_channel, input_topic, logic_type, logic_output, logic_value, output_channel,
output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
@@ -271,7 +280,7 @@ func (repo *PostgresRepository) ListRules(ctx context.Context, pm re.PageMeta) (
return ret, nil
}
func pageQuery(pm re.PageMeta) string {
func pageRulesQuery(pm re.PageMeta) string {
var query []string
if pm.InputChannel != "" {
query = append(query, "r.input_channel = :input_channel")
@@ -297,3 +306,277 @@ func pageQuery(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 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.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
}
+115 -1
View File
@@ -37,6 +37,26 @@ type dbRule struct {
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 time.Time `db:"start_datetime"`
Time time.Time `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"`
}
func ruleToDb(r re.Rule) (dbRule, error) {
metadata := []byte("{}")
if len(r.Metadata) > 0 {
@@ -105,7 +125,7 @@ func dbToRule(dto dbRule) (re.Rule, error) {
Recurring: dto.Recurring,
RecurringPeriod: dto.RecurringPeriod,
},
Status: re.Status(dto.Status),
Status: dto.Status,
CreatedAt: dto.CreatedAt,
CreatedBy: dto.CreatedBy,
UpdatedAt: dto.UpdatedAt,
@@ -113,6 +133,100 @@ 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
}
return dbReport{
ID: r.ID,
Name: r.Name,
Description: r.Description,
DomainID: r.DomainID,
StartDateTime: r.Schedule.StartDateTime,
Time: r.Schedule.Time,
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.Metric
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: dto.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}
+360
View File
@@ -0,0 +1,360 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package re
import (
"encoding/json"
"fmt"
"net/mail"
"strings"
"time"
"github.com/absmach/magistrala/pkg/reltime"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/transformers/senml"
)
var (
errFromTimeNotProvided = errors.New("\"from time\" not provided")
errInvalidFromTime = errors.New("invalid \"from time\" ")
errToTimeNotProvided = errors.New("\"to time\" not provided")
errInvalidToTime = errors.New("invalid \"to time\"")
errAggIntervalTimeNotProvided = errors.New("aggregation interval time not provided")
errInvalidAggInterval = errors.New("invalid aggregation interval time")
errNoToEmail = errors.New("no \"To\" email address found")
errChannelIDNotProvided = errors.New("channel id not provided")
errClientIDNotProvided = errors.New("client id not provided")
errNameNotProvided = errors.New("name not provided")
)
const (
errInvalidFormatFmt = "invalid format %s"
errInvalidReportActionFmt = "invalid action %s"
errInvalidToEmail = "invalid \"To\" email %s"
errUnknownAggregationFmt = "unknown aggregation type %d"
errUnknownAggregationStringFmt = "unknown aggregation type %s"
)
type Report struct {
Metric Metric `json:"metric,omitempty"`
Messages []senml.Message `json:"messages,omitempty"`
}
type ReportPage struct {
Total uint64 `json:"total"`
From time.Time `json:"from,omitempty"`
To time.Time `json:"to,omitempty"`
Aggregation AggConfig `json:"aggregation,omitempty"`
Reports []Report `json:"reports,omitempty"`
File ReportFile `json:"file,omitempty"`
}
type ReportFile struct {
Name string `json:"name,omitempty"`
Data []byte `json:"data,omitempty"`
Format Format `json:"format,omitempty"`
}
type AggConfig struct {
AggType Aggregation `json:"agg_type,omitempty"` // Optional field
Interval string `json:"interval,omitempty"` // Mandatory field if "AggType" field is set MAX, MIN, COUNT, SUM, AVG
}
func (ac AggConfig) Validate() error {
if ac.AggType != AggregationNONE {
if ac.Interval == "" {
return errAggIntervalTimeNotProvided
}
if _, err := time.ParseDuration(ac.Interval); err != nil {
return errInvalidAggInterval
}
}
return nil
}
type MetricConfig struct {
From string `json:"from,omitempty"` // Mandatory field
To string `json:"to,omitempty"` // Mandatory field
FileFormat Format `json:"file_format,omitempty"` // Optional field
Aggregation AggConfig `json:"aggregation,omitempty"` // Optional field
}
func (mc MetricConfig) Validate() error {
if mc.From == "" {
return errFromTimeNotProvided
}
if _, err := reltime.Parse(mc.From); err != nil {
return errInvalidFromTime
}
if mc.To == "" {
return errToTimeNotProvided
}
if _, err := reltime.Parse(mc.To); err != nil {
return errInvalidToTime
}
if err := mc.Aggregation.Validate(); err != nil {
return err
}
return nil
}
type Metric struct {
ChannelID string `json:"channel_id,omitempty"` // Mandatory field
ClientID string `json:"client_id,omitempty"` // Mandatory field
Name string `json:"name,omitempty"` // Mandatory field
Subtopic string `json:"subtopic,omitempty"` // Optional field
Protocol string `json:"protocol,omitempty"` // Optional field
Format string `json:"format,omitiempty"` // Optional field
}
func (m Metric) Validate() error {
if m.ChannelID == "" {
return errChannelIDNotProvided
}
if m.ClientID == "" {
return errClientIDNotProvided
}
if m.Name == "" {
return errNameNotProvided
}
return nil
}
type ReportConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DomainID string `json:"domain_id"`
Schedule Schedule `json:"schedule,omitempty"`
Config *MetricConfig `json:"config,omitempty"`
Email *EmailSetting `json:"email,omitempty"`
Metrics []Metric `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 {
PageMeta
ReportConfigs []ReportConfig `json:"report_configs"`
}
type EmailSetting struct {
To []string `json:"to,omitempty"`
Subject string `json:"subject,omitempty"`
Content string `json:"content,omitempty"`
}
func (es *EmailSetting) Validate() error {
if len(es.To) == 0 {
return errNoToEmail
}
for _, to := range es.To {
if _, err := mail.ParseAddress(to); err != nil {
return errors.Wrap(fmt.Errorf(errInvalidToEmail, to), err)
}
}
return nil
}
type Format uint8
const (
PDF = iota
CSV
AllFormats
)
const (
PdfFormat = "pdf"
CsvFormat = "csv"
All_Formats = "AllFormats"
)
func (f Format) String() string {
switch f {
case PDF:
return PdfFormat
case CSV:
return CsvFormat
case AllFormats:
return All_Formats
default:
return Unknown
}
}
func (f Format) Extension() string {
switch f {
case PDF:
return PdfFormat
case CSV:
return CsvFormat
default:
return Unknown
}
}
func (f Format) ContentType() string {
switch f {
case PDF:
return "application/pdf"
case CSV:
return "text/csv"
default:
return Unknown
}
}
func ToFormat(format string) (Format, error) {
switch format {
case "", PdfFormat:
return PDF, nil
case CsvFormat:
return CSV, nil
case All_Formats:
return AllFormats, nil
}
return Format(0), fmt.Errorf(errInvalidFormatFmt, format)
}
func (f Format) MarshalJSON() ([]byte, error) {
return json.Marshal(f.String())
}
func (f *Format) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToFormat(str)
*f = val
return err
}
type ReportAction uint8
const (
ViewReport = iota
DownloadReport
EmailReport
)
const (
ViewReportAction = "view"
DownloadReportAction = "download"
EmailReportAction = "email"
)
func (ra ReportAction) String() string {
switch ra {
case ViewReport:
return ViewReportAction
case DownloadReport:
return DownloadReportAction
case EmailReport:
return EmailReportAction
default:
return Unknown
}
}
func ToReportAction(action string) (ReportAction, error) {
switch action {
case "", ViewReportAction:
return ViewReport, nil
case DownloadReportAction:
return DownloadReport, nil
case EmailReportAction:
return EmailReport, nil
}
return ReportAction(0), fmt.Errorf(errInvalidReportActionFmt, action)
}
func (ra ReportAction) MarshalJSON() ([]byte, error) {
return json.Marshal(ra.String())
}
func (ra *ReportAction) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToReportAction(str)
*ra = val
return err
}
type Aggregation uint8
const (
AggregationNONE = iota
AggregationMAX
AggregationMIN
AggregationSUM
AggregationCOUNT
AggregationAVG
)
const (
aggregationNONE = "none"
aggregationMAX = "max"
aggregationMIN = "min"
aggregationSUM = "sum"
aggregationCOUNT = "count"
aggregationAVG = "avg"
)
func (a Aggregation) String() string {
switch a {
case AggregationNONE:
return aggregationNONE
case AggregationMAX:
return aggregationMAX
case AggregationMIN:
return aggregationMIN
case AggregationSUM:
return aggregationSUM
case AggregationCOUNT:
return aggregationCOUNT
case AggregationAVG:
return aggregationAVG
default:
return fmt.Sprintf(errUnknownAggregationFmt, a)
}
}
func ToAggregation(agg string) (Aggregation, error) {
switch strings.ToLower(agg) {
case "", aggregationNONE:
return AggregationNONE, nil
case aggregationMAX:
return AggregationMAX, nil
case aggregationMIN:
return AggregationMIN, nil
case aggregationSUM:
return AggregationSUM, nil
case aggregationCOUNT:
return AggregationCOUNT, nil
case aggregationAVG:
return AggregationAVG, nil
default:
return Aggregation(0), fmt.Errorf(errUnknownAggregationStringFmt, agg)
}
}
func (a Aggregation) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
func (a *Aggregation) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToAggregation(str)
*a = val
return err
}
+354 -4
View File
@@ -5,23 +5,38 @@ 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/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)
UpdateRule(ctx context.Context, r Rule) (Rule, error)
UpdateRuleSchedule(ctx context.Context, r Rule) (Rule, error)
RemoveRule(ctx context.Context, id string) error
UpdateRuleStatus(ctx context.Context, id string, status Status) (Rule, error)
UpdateRuleStatus(ctx context.Context, r Rule) (Rule, error)
ListRules(ctx context.Context, pm PageMeta) (Page, 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)
}
// PageMeta contains page metadata that helps navigation.
@@ -58,6 +73,17 @@ type Service interface {
RemoveRule(ctx context.Context, session authn.Session, id string) error
EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error)
DisableRule(ctx context.Context, session authn.Session, id string) (Rule, error)
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
}
@@ -70,9 +96,10 @@ type re struct {
alarmsPub messaging.Publisher
ticker Ticker
email Emailer
readers grpcReadersV1.ReadersServiceClient
}
func NewService(repo Repository, errors chan (error), idp supermq.IDProvider, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, tck Ticker, emailer Emailer) Service {
func NewService(repo Repository, errors chan (error), idp supermq.IDProvider, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, tck Ticker, emailer Emailer, readers grpcReadersV1.ReadersServiceClient) Service {
return &re{
repo: repo,
idp: idp,
@@ -82,6 +109,7 @@ func NewService(repo Repository, errors chan (error), idp supermq.IDProvider, re
alarmsPub: alarmsPub,
ticker: tck,
email: emailer,
readers: readers,
}
}
@@ -162,7 +190,13 @@ func (re *re) EnableRule(ctx context.Context, session authn.Session, id string)
if err != nil {
return Rule{}, err
}
rule, err := re.repo.UpdateRuleStatus(ctx, id, status)
r := Rule{
ID: id,
UpdatedAt: time.Now(),
UpdatedBy: session.UserID,
Status: status,
}
rule, err := re.repo.UpdateRuleStatus(ctx, r)
if err != nil {
return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
@@ -174,7 +208,13 @@ func (re *re) DisableRule(ctx context.Context, session authn.Session, id string)
if err != nil {
return Rule{}, err
}
rule, err := re.repo.UpdateRuleStatus(ctx, id, status)
r := Rule{
ID: id,
UpdatedAt: time.Now(),
UpdatedBy: session.UserID,
Status: status,
}
rule, err := re.repo.UpdateRuleStatus(ctx, r)
if err != nil {
return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
@@ -188,3 +228,313 @@ func (re *re) Cancel() error {
func (re *re) Errors() <-chan error {
return re.errors
}
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
}
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()
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()
cfg.UpdatedBy = session.UserID
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(),
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(),
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 reports []Report
for _, metric := range cfg.Metrics {
sMsgs := []senml.Message{}
pm.Offset = uint64(0)
pm.Publisher = metric.ClientID
pm.Name = metric.Name
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, Report{
Metric: metric,
Messages: sMsgs,
})
}
switch {
case genReportFile != nil:
data, err := genReportFile(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([]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 {
return senml.Message{
Protocol: g.Base.GetProtocol(),
Subtopic: g.Base.GetSubtopic(),
Unit: g.GetUnit(),
Time: g.GetTime(),
UpdateTime: g.GetUpdateTime(),
Name: g.GetName(),
Value: g.Value,
StringValue: g.StringValue,
DataValue: g.DataValue,
BoolValue: g.BoolValue,
Sum: g.Sum,
}
}
+438 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/absmach/magistrala/internal/testsutil"
"github.com/absmach/magistrala/re"
"github.com/absmach/magistrala/re/mocks"
readmocks "github.com/absmach/magistrala/readers/mocks"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
@@ -54,6 +55,17 @@ var (
Recurring: re.None,
},
}
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, errs chan error) (re.Service, *mocks.Repository, *pubsubmocks.PubSub, *mocks.Ticker) {
@@ -61,8 +73,9 @@ func newService(t *testing.T, errs chan error) (re.Service, *mocks.Repository, *
mockTicker := new(mocks.Ticker)
idProvider := uuid.NewMock()
pubsub := pubsubmocks.NewPubSub(t)
readersSvc := new(readmocks.ReadersServiceClient)
e := new(mocks.Emailer)
return re.NewService(repo, errs, idProvider, pubsub, pubsub, pubsub, mockTicker, e), repo, pubsub, mockTicker
return re.NewService(repo, errs, idProvider, pubsub, pubsub, pubsub, mockTicker, e, readersSvc), repo, pubsub, mockTicker
}
func TestAddRule(t *testing.T) {
@@ -431,6 +444,8 @@ func TestRemoveRule(t *testing.T) {
func TestEnableRule(t *testing.T) {
svc, repo, _, _ := newService(t, make(chan error))
now := time.Now()
cases := []struct {
desc string
session authn.Session
@@ -454,6 +469,8 @@ func TestEnableRule(t *testing.T) {
InputChannel: inputChannel,
Status: re.EnabledStatus,
Schedule: schedule,
UpdatedBy: userID,
UpdatedAt: now,
},
err: nil,
},
@@ -471,7 +488,7 @@ func TestEnableRule(t *testing.T) {
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("UpdateRuleStatus", context.Background(), tc.id, tc.status).Return(tc.res, tc.err)
repoCall := repo.On("UpdateRuleStatus", context.Background(), mock.Anything).Return(tc.res, tc.err)
res, err := svc.EnableRule(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
@@ -486,6 +503,8 @@ func TestEnableRule(t *testing.T) {
func TestDisableRule(t *testing.T) {
svc, repo, _, _ := newService(t, make(chan error))
now := time.Now()
cases := []struct {
desc string
session authn.Session
@@ -509,6 +528,8 @@ func TestDisableRule(t *testing.T) {
InputChannel: inputChannel,
Status: re.DisabledStatus,
Schedule: schedule,
UpdatedBy: userID,
UpdatedAt: now,
},
err: nil,
},
@@ -526,7 +547,7 @@ func TestDisableRule(t *testing.T) {
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("UpdateRuleStatus", mock.Anything, tc.id, tc.status).Return(tc.res, tc.err)
repoCall := repo.On("UpdateRuleStatus", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.DisableRule(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
@@ -608,6 +629,7 @@ 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)
@@ -616,6 +638,417 @@ func TestHandle(t *testing.T) {
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
})
}
}
func TestAddReportConfig(t *testing.T) {
svc, repo, _, _ := newService(t, make(chan error))
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 error))
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 error))
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 error))
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 error))
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 error))
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 error))
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()
})
}
}
@@ -821,6 +1254,7 @@ func TestStartScheduler(t *testing.T) {
for _, tc := range cases {
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()
@@ -850,6 +1284,7 @@ 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()
})
+5 -5
View File
@@ -194,11 +194,11 @@ func TestReadMessages(t *testing.T) {
Unit: "C",
Time: 1672531200,
UpdateTime: 1672531300,
Value: 22.5,
StringValue: "ok",
DataValue: "binary",
BoolValue: true,
Sum: 123.4,
Value: float64Ptr(22.5),
StringValue: stringPtr("ok"),
DataValue: stringPtr("binary"),
BoolValue: boolPtr(true),
Sum: float64Ptr(123.4),
},
},
},
+5 -26
View File
@@ -97,11 +97,11 @@ func toResponseMessages(messages []readers.Message) []*grpcReadersV1.Message {
Unit: typed.Unit,
Time: typed.Time,
UpdateTime: typed.UpdateTime,
Value: derefFloat64(typed.Value),
StringValue: derefString(typed.StringValue),
DataValue: derefString(typed.DataValue),
BoolValue: derefBool(typed.BoolValue),
Sum: derefFloat64(typed.Sum),
Value: typed.Value,
StringValue: typed.StringValue,
DataValue: typed.DataValue,
BoolValue: typed.BoolValue,
Sum: typed.Sum,
},
},
})
@@ -149,27 +149,6 @@ func stringifyAggregation(agg grpcReadersV1.Aggregation) string {
}
}
func derefString(s *string) string {
if s == nil {
return ""
}
return *s
}
func derefFloat64(f *float64) float64 {
if f == nil {
return 0
}
return *f
}
func derefBool(b *bool) bool {
if b == nil {
return false
}
return *b
}
func safeString(v interface{}) string {
if s, ok := v.(string); ok {
return s
+114
View File
@@ -0,0 +1,114 @@
// 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"
v1 "github.com/absmach/magistrala/api/grpc/readers/v1"
mock "github.com/stretchr/testify/mock"
"google.golang.org/grpc"
)
// NewReadersServiceClient creates a new instance of ReadersServiceClient. 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 NewReadersServiceClient(t interface {
mock.TestingT
Cleanup(func())
}) *ReadersServiceClient {
mock := &ReadersServiceClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// ReadersServiceClient is an autogenerated mock type for the ReadersServiceClient type
type ReadersServiceClient struct {
mock.Mock
}
type ReadersServiceClient_Expecter struct {
mock *mock.Mock
}
func (_m *ReadersServiceClient) EXPECT() *ReadersServiceClient_Expecter {
return &ReadersServiceClient_Expecter{mock: &_m.Mock}
}
// ReadMessages provides a mock function for the type ReadersServiceClient
func (_mock *ReadersServiceClient) ReadMessages(ctx context.Context, in *v1.ReadMessagesReq, opts ...grpc.CallOption) (*v1.ReadMessagesRes, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ReadMessages")
}
var r0 *v1.ReadMessagesRes
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ReadMessagesReq, []grpc.CallOption) (*v1.ReadMessagesRes, error)); ok {
return returnFunc(ctx, in, opts)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ReadMessagesReq, ...grpc.CallOption) *v1.ReadMessagesRes); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.ReadMessagesRes)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.ReadMessagesReq, ...grpc.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ReadersServiceClient_ReadMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadMessages'
type ReadersServiceClient_ReadMessages_Call struct {
*mock.Call
}
// ReadMessages is a helper method to define mock.On call
// - ctx
// - in
// - opts
func (_e *ReadersServiceClient_Expecter) ReadMessages(ctx interface{}, in interface{}, opts ...interface{}) *ReadersServiceClient_ReadMessages_Call {
return &ReadersServiceClient_ReadMessages_Call{Call: _e.mock.On("ReadMessages",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *ReadersServiceClient_ReadMessages_Call) Run(run func(ctx context.Context, in *v1.ReadMessagesReq, opts ...grpc.CallOption)) *ReadersServiceClient_ReadMessages_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]grpc.CallOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(grpc.CallOption)
}
}
run(args[0].(context.Context), args[1].(*v1.ReadMessagesReq), variadicArgs...)
})
return _c
}
func (_c *ReadersServiceClient_ReadMessages_Call) Return(readMessagesRes *v1.ReadMessagesRes, err error) *ReadersServiceClient_ReadMessages_Call {
_c.Call.Return(readMessagesRes, err)
return _c
}
func (_c *ReadersServiceClient_ReadMessages_Call) RunAndReturn(run func(ctx context.Context, in *v1.ReadMessagesReq, opts ...grpc.CallOption) (*v1.ReadMessagesRes, error)) *ReadersServiceClient_ReadMessages_Call {
_c.Call.Return(run)
return _c
}
+9 -1
View File
@@ -21,6 +21,7 @@ packages:
Repository:
Service:
Ticker:
Emailer:
github.com/absmach/magistrala/bootstrap:
interfaces:
ConfigRepository:
@@ -36,4 +37,11 @@ packages:
github.com/absmach/magistrala/alarms:
interfaces:
Service:
Repository:
Repository:
github.com/absmach/magistrala/api/grpc/readers/v1:
interfaces:
ReadersServiceClient:
config:
dir: "./readers/mocks"
mockname: "ReadersServiceClient"
filename: "readers_client.go"