From 8b4766d740d33c11a4f0e78e8d32a7afb30783ab Mon Sep 17 00:00:00 2001 From: Steve Munene Date: Wed, 6 Aug 2025 11:18:36 +0300 Subject: [PATCH] NOISSUE - Add reports template tests (#264) * fix template tests Signed-off-by: nyagamunene * fix failing linter Signed-off-by: nyagamunene * add endpoint tests Signed-off-by: nyagamunene * fix failing linter Signed-off-by: nyagamunene --------- Signed-off-by: nyagamunene --- .github/workflows/tests.yaml | 5 + reports/api/endpoints_test.go | 569 ++++++++++++++++++++++++++++++++++ reports/api/request.go | 2 +- reports/template_test.go | 376 ++++++++++++++++++++++ 4 files changed, 951 insertions(+), 1 deletion(-) create mode 100644 reports/template_test.go diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8ba3db0a5..105a8cc8e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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: | diff --git a/reports/api/endpoints_test.go b/reports/api/endpoints_test.go index 9894da86b..b42330872 100644 --- a/reports/api/endpoints_test.go +++ b/reports/api/endpoints_test.go @@ -832,3 +832,572 @@ type respBody struct { ID string `json:"id"` Status reports.Status `json:"status"` } + +const ( + validTemplate = ` + + + {{$.Title}} + + + +
+

{{$.Title}}

+

Generated on: {{$.GeneratedDate}}

+
+
+

Messages

+ {{range .Messages}} +
+

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+
+ {{end}} +
+ +` + + templateWithoutTitle = ` + + + Report + + + +

Report

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end}} + +` + + templateWithSyntaxError = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end + +` +) + +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() + }) + } +} diff --git a/reports/api/request.go b/reports/api/request.go index 216a16cc3..675206e50 100644 --- a/reports/api/request.go +++ b/reports/api/request.go @@ -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) diff --git a/reports/template_test.go b/reports/template_test.go new file mode 100644 index 000000000..69ab33a25 --- /dev/null +++ b/reports/template_test.go @@ -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 = ` + + + {{$.Title}} + + + +
+

{{$.Title}}

+

Generated on: {{$.GeneratedDate}}

+
+
+

Messages

+ {{range .Messages}} +
+

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+
+ {{end}} +
+ +` + + templateWithoutTitle = ` + + + Report + + + +

Report

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end}} + +` + + templateWithoutRange = ` + + + {{$.Title}} + + +

{{$.Title}}

+

No messages to display

+ +` + + templateWithoutFormatTime = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{range .Messages}} +

Time: {{.Time}}

+

Value: {{formatValue .}}

+ {{end}} + +` + + templateWithoutFormatValue = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{.}}

+ {{end}} + +` + + templateWithoutEnd = ` + + + {{$.Title}} + + +

{{$.Title}}

+

Time: {{formatTime "test"}}

+

Value: {{formatValue "test"}}

+

No range block with end

+ +` + + templateWithSyntaxError = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end + +` + + templateWithUndefinedFunction = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+

Custom: {{customFunction .}}

+ {{end}} + +` + + templateWithIfCondition = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{if .Messages}} + {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end}} + {{else}} +

No messages available

+ {{end}} + +` + + templateWithWithCondition = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{with .Data}} + {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end}} + {{else}} +

No data available

+ {{end}} + +` + + templateWithNestedConditions = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{if .HasMessages}} + {{with .Data}} + {{range .Messages}} +

Time: {{formatTime .Time}}

+

Value: {{formatValue .}}

+ {{end}} + {{else}} +

Data not available

+ {{end}} + {{else}} +

No messages flag set

+ {{end}} + +` + + templateWithIfMissingFields = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{if .Messages}} + {{range .Messages}} +

Time: {{.Time}}

+

Value: {{.}}

+ {{end}} + {{else}} +

No messages available

+ {{end}} + +` + + templateWithWithMissingFields = ` + + + {{$.Title}} + + +

{{$.Title}}

+ {{with .Data}} + {{range .Messages}} +

Time: {{.Time}}

+

Value: {{formatValue .}}

+ {{end}} + {{else}} +

No data available

+ {{end}} + +` +) + +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)) + } + }) + } +}