mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 04:10:34 +00:00
NOISSUE - Add reports template tests (#264)
* fix template tests Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * add endpoint tests Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> --------- Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
@@ -180,6 +180,11 @@ jobs:
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/re.out ./re/...
|
||||
|
||||
- name: Run reports tests
|
||||
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/reports.out ./reports/...
|
||||
|
||||
- name: Run alarms tests
|
||||
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
|
||||
@@ -832,3 +832,572 @@ type respBody struct {
|
||||
ID string `json:"id"`
|
||||
Status reports.Status `json:"status"`
|
||||
}
|
||||
|
||||
const (
|
||||
validTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.header { background-color: #f0f0f0; padding: 10px; }
|
||||
.content { padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>{{$.Title}}</h1>
|
||||
<p>Generated on: {{$.GeneratedDate}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Messages</h2>
|
||||
{{range .Messages}}
|
||||
<div class="message">
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutTitle = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Report</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Report</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithSyntaxError = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
func TestUpdateReportTemplateEndpoint(t *testing.T) {
|
||||
ts, svc, authn := newReportsServer()
|
||||
defer ts.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
template reports.ReportTemplate
|
||||
domainID string
|
||||
token string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update report template successfully",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "update report template with invalid token",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: invalidToken,
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
authnErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "update report template with empty token",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: "",
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "update report template with empty domainID",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "update report template with invalid content type",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
contentType: "application/xml",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
err: apiutil.ErrUnsupportedContentType,
|
||||
},
|
||||
{
|
||||
desc: "update report template with empty ID",
|
||||
id: "",
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "update report template with empty template",
|
||||
id: validID,
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "update report template without title field",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(templateWithoutTitle),
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "update report template with syntax error",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(templateWithSyntaxError),
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "update report template with service error",
|
||||
id: validID,
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
contentType: contentType,
|
||||
svcErr: svcerr.ErrUpdateEntity,
|
||||
status: http.StatusUnprocessableEntity,
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
data := toJSON(map[string]interface{}{
|
||||
"report_template": tc.template,
|
||||
})
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(data),
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("UpdateReportTemplate", mock.Anything, tc.authnRes, mock.Anything).Return(tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
var errRes respBody
|
||||
err = json.NewDecoder(res.Body).Decode(&errRes)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
|
||||
if errRes.Err != "" || errRes.Message != "" {
|
||||
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewReportTemplateEndpoint(t *testing.T) {
|
||||
ts, svc, authn := newReportsServer()
|
||||
defer ts.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
token string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcRes reports.ReportTemplate
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view report template successfully",
|
||||
id: validID,
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
status: http.StatusOK,
|
||||
svcRes: reports.ReportTemplate(validTemplate),
|
||||
},
|
||||
{
|
||||
desc: "view report template with invalid token",
|
||||
id: validID,
|
||||
token: invalidToken,
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
authnErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "view report template with empty token",
|
||||
token: "",
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "view report template with empty domainID",
|
||||
token: validToken,
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "view report template with empty ID",
|
||||
token: validToken,
|
||||
id: "",
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "view report template with service error",
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
svcErr: svcerr.ErrViewEntity,
|
||||
status: http.StatusBadRequest,
|
||||
err: svcerr.ErrViewEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.token,
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("ViewReportTemplate", mock.Anything, tc.authnRes, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var errRes respBody
|
||||
err = json.NewDecoder(res.Body).Decode(&errRes)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
|
||||
if errRes.Err != "" || errRes.Message != "" {
|
||||
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteReportTemplateEndpoint(t *testing.T) {
|
||||
ts, svc, authn := newReportsServer()
|
||||
defer ts.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
token string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "delete report template successfully",
|
||||
id: validID,
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with invalid token",
|
||||
id: validID,
|
||||
token: invalidToken,
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
authnErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with empty token",
|
||||
token: "",
|
||||
authnRes: smqauthn.Session{},
|
||||
domainID: domainID,
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with empty domainID",
|
||||
token: validToken,
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with empty ID",
|
||||
token: validToken,
|
||||
id: "",
|
||||
domainID: domainID,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with service error",
|
||||
token: validToken,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
id: validID,
|
||||
contentType: contentType,
|
||||
svcErr: svcerr.ErrRemoveEntity,
|
||||
status: http.StatusUnprocessableEntity,
|
||||
err: svcerr.ErrRemoveEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%s/reports/configs/%s/template", ts.URL, tc.domainID, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.token,
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("DeleteReportTemplate", mock.Anything, tc.authnRes, tc.id).Return(tc.svcErr)
|
||||
res, err := req.make()
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
var errRes respBody
|
||||
err = json.NewDecoder(res.Body).Decode(&errRes)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
|
||||
if errRes.Err != "" || errRes.Message != "" {
|
||||
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportWithTemplateValidation(t *testing.T) {
|
||||
ts, svc, authn := newReportsServer()
|
||||
defer ts.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg reports.ReportConfig
|
||||
action string
|
||||
domainID string
|
||||
token string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcRes reports.ReportPage
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "generate report with valid template successfully",
|
||||
cfg: reports.ReportConfig{
|
||||
ID: validID,
|
||||
Name: namegen.Generate(),
|
||||
DomainID: domainID,
|
||||
Metrics: []reports.ReqMetric{
|
||||
{
|
||||
ChannelID: "channel1",
|
||||
ClientIDs: []string{"client1"},
|
||||
Name: "metric_name",
|
||||
},
|
||||
},
|
||||
Config: &reports.MetricConfig{
|
||||
From: "now()-1h",
|
||||
To: "now()",
|
||||
Title: title,
|
||||
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
|
||||
},
|
||||
ReportTemplate: reports.ReportTemplate(validTemplate),
|
||||
},
|
||||
action: "view",
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
|
||||
status: http.StatusCreated,
|
||||
svcRes: reports.ReportPage{},
|
||||
},
|
||||
{
|
||||
desc: "generate report with invalid template",
|
||||
cfg: reports.ReportConfig{
|
||||
ID: validID,
|
||||
Name: namegen.Generate(),
|
||||
DomainID: domainID,
|
||||
Metrics: []reports.ReqMetric{
|
||||
{
|
||||
ChannelID: "channel1",
|
||||
ClientIDs: []string{"client1"},
|
||||
Name: "metric_name",
|
||||
},
|
||||
},
|
||||
Config: &reports.MetricConfig{
|
||||
From: "now()-1h",
|
||||
To: "now()",
|
||||
Title: title,
|
||||
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
|
||||
},
|
||||
ReportTemplate: reports.ReportTemplate(templateWithoutTitle),
|
||||
},
|
||||
action: "view",
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "generate report with template syntax error",
|
||||
cfg: reports.ReportConfig{
|
||||
ID: validID,
|
||||
Name: namegen.Generate(),
|
||||
DomainID: domainID,
|
||||
Metrics: []reports.ReqMetric{
|
||||
{
|
||||
ChannelID: "channel1",
|
||||
ClientIDs: []string{"client1"},
|
||||
Name: "metric_name",
|
||||
},
|
||||
},
|
||||
Config: &reports.MetricConfig{
|
||||
From: "now()-1h",
|
||||
To: "now()",
|
||||
Title: title,
|
||||
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
|
||||
},
|
||||
ReportTemplate: reports.ReportTemplate(templateWithSyntaxError),
|
||||
},
|
||||
action: "view",
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
domainID: domainID,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
data := toJSON(tc.cfg)
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/%s/reports?action=%s", ts.URL, tc.domainID, tc.action),
|
||||
contentType: tc.contentType,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(data),
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("GenerateReport", mock.Anything, tc.authnRes, mock.Anything, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var errRes respBody
|
||||
err = json.NewDecoder(res.Body).Decode(&errRes)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
|
||||
if errRes.Err != "" || errRes.Message != "" {
|
||||
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func (req updateReportTemplateReq) validate() error {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.ReportTemplate == "" {
|
||||
return errors.Wrap(apiutil.ErrValidation, errMissingReportTemplate)
|
||||
return errors.Wrap(errMissingReportTemplate, apiutil.ErrValidation)
|
||||
}
|
||||
if err := req.ReportTemplate.Validate(); err != nil {
|
||||
return errors.Wrap(err, apiutil.ErrValidation)
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package reports_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/reports"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
validTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.header { background-color: #f0f0f0; padding: 10px; }
|
||||
.content { padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>{{$.Title}}</h1>
|
||||
<p>Generated on: {{$.GeneratedDate}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Messages</h2>
|
||||
{{range .Messages}}
|
||||
<div class="message">
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutTitle = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Report</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Report</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutRange = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
<p>No messages to display</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutFormatTime = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{.Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutFormatValue = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{.}}</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithoutEnd = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
<p>Time: {{formatTime "test"}}</p>
|
||||
<p>Value: {{formatValue "test"}}</p>
|
||||
<p>No range block with end</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithSyntaxError = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithUndefinedFunction = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
<p>Custom: {{customFunction .}}</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithIfCondition = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{if .Messages}}
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>No messages available</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithWithCondition = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{with .Data}}
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>No data available</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithNestedConditions = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{if .HasMessages}}
|
||||
{{with .Data}}
|
||||
{{range .Messages}}
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>Data not available</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>No messages flag set</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithIfMissingFields = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{if .Messages}}
|
||||
{{range .Messages}}
|
||||
<p>Time: {{.Time}}</p>
|
||||
<p>Value: {{.}}</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>No messages available</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
templateWithWithMissingFields = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{$.Title}}</h1>
|
||||
{{with .Data}}
|
||||
{{range .Messages}}
|
||||
<p>Time: {{.Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>No data available</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
func TestReportTemplate_Validate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
template reports.ReportTemplate
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "validate template successfully",
|
||||
template: reports.ReportTemplate(validTemplate),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "validate template without title field",
|
||||
template: reports.ReportTemplate(templateWithoutTitle),
|
||||
err: fmt.Errorf("missing essential template field: {{$.Title}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template without range field",
|
||||
template: reports.ReportTemplate(templateWithoutRange),
|
||||
err: fmt.Errorf("missing essential template field: {{range .Messages}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template without formatTime field",
|
||||
template: reports.ReportTemplate(templateWithoutFormatTime),
|
||||
err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template without formatValue field",
|
||||
template: reports.ReportTemplate(templateWithoutFormatValue),
|
||||
err: fmt.Errorf("missing essential template field: {{formatValue .}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template without end field",
|
||||
template: reports.ReportTemplate(templateWithoutEnd),
|
||||
err: fmt.Errorf("missing essential template field: {{range .Messages}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template with syntax error",
|
||||
template: reports.ReportTemplate(templateWithSyntaxError),
|
||||
err: fmt.Errorf("template syntax error"),
|
||||
},
|
||||
{
|
||||
desc: "validate template with undefined function",
|
||||
template: reports.ReportTemplate(templateWithUndefinedFunction),
|
||||
err: fmt.Errorf("template syntax error"),
|
||||
},
|
||||
{
|
||||
desc: "validate empty template",
|
||||
template: reports.ReportTemplate(""),
|
||||
err: fmt.Errorf("missing essential template field: {{$.Title}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template with if condition successfully",
|
||||
template: reports.ReportTemplate(templateWithIfCondition),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "validate template `with` with condition successfully",
|
||||
template: reports.ReportTemplate(templateWithWithCondition),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "validate template with nested conditions successfully",
|
||||
template: reports.ReportTemplate(templateWithNestedConditions),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "validate template with if condition missing formatTime",
|
||||
template: reports.ReportTemplate(templateWithIfMissingFields),
|
||||
err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"),
|
||||
},
|
||||
{
|
||||
desc: "validate template `with` with condition missing formatTime",
|
||||
template: reports.ReportTemplate(templateWithWithMissingFields),
|
||||
err: fmt.Errorf("missing essential template field: {{formatTime .Time}}"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.template.Validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportTemplate_String(t *testing.T) {
|
||||
template := reports.ReportTemplate(validTemplate)
|
||||
result := template.String()
|
||||
|
||||
assert.Equal(t, validTemplate, result)
|
||||
}
|
||||
|
||||
func TestReportTemplate_MarshalJSON(t *testing.T) {
|
||||
template := reports.ReportTemplate("simple template")
|
||||
data, err := template.MarshalJSON()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, data)
|
||||
assert.Equal(t, `"simple template"`, string(data))
|
||||
}
|
||||
|
||||
func TestReportTemplate_UnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
data []byte
|
||||
expected string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "unmarshal valid JSON successfully",
|
||||
data: []byte(`"simple template"`),
|
||||
expected: "simple template",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "unmarshal invalid JSON",
|
||||
data: []byte(`invalid json`),
|
||||
err: fmt.Errorf("invalid character"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var template reports.ReportTemplate
|
||||
err := template.UnmarshalJSON(tc.data)
|
||||
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, string(template))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user