mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:50:18 +00:00
NOISSUE - Refactor reports template validation (#260)
* fix validation Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * remove unused code Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * NOISSUE - Bump github.com/nats-io/nats.go from 1.43.0 to 1.44.0 (#262) Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.43.0 to 1.44.0. - [Release notes](https://github.com/nats-io/nats.go/releases) - [Commits](https://github.com/nats-io/nats.go/compare/v1.43.0...v1.44.0) --- updated-dependencies: - dependency-name: github.com/nats-io/nats.go dependency-version: 1.44.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * NOISSUE - Update to using OpenBao in certs service (#259) * chore: update smq dep Signed-off-by: Felix Gateru <felix.gateru@gmail.com> * chore: remove am-certs and replace with open-bao Signed-off-by: Felix Gateru <felix.gateru@gmail.com> * refactor: remove vault references Signed-off-by: Felix Gateru <felix.gateru@gmail.com> --------- Signed-off-by: Felix Gateru <felix.gateru@gmail.com> * NOISSUE - Bump github.com/prometheus/client_golang from 1.22.0 to 1.23.0 (#263) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.22.0 to 1.23.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.23.0/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.22.0...v1.23.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-version: 1.23.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * address comments Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * remove walk node method Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * address comments Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> --------- Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Felix Gateru <felix.gateru@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
+100
-93
@@ -6,48 +6,8 @@ package reports
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
requiredFields = []string{
|
||||
"{{$.Title}}",
|
||||
"{{$.GeneratedDate}}",
|
||||
"{{$.GeneratedTime}}",
|
||||
"{{.Metric.Name}}",
|
||||
"{{.Metric.ClientID}}",
|
||||
"{{.Metric.ChannelID}}",
|
||||
"{{len .Messages}}",
|
||||
"{{range .Messages}}",
|
||||
"{{formatTime .Time}}",
|
||||
"{{formatValue .}}",
|
||||
"{{.Unit}}",
|
||||
"{{.Protocol}}",
|
||||
"{{.Subtopic}}",
|
||||
"{{end}}",
|
||||
}
|
||||
|
||||
requiredStructure = []string{
|
||||
"<!DOCTYPE html>",
|
||||
"<html",
|
||||
"<head>",
|
||||
"<body>",
|
||||
"<style>",
|
||||
"</style>",
|
||||
"</head>",
|
||||
"</body>",
|
||||
"</html>",
|
||||
}
|
||||
|
||||
requiredCSS = []string{
|
||||
".page",
|
||||
".header",
|
||||
".content-area",
|
||||
".metrics-section",
|
||||
".data-table",
|
||||
".footer",
|
||||
}
|
||||
"text/template"
|
||||
"text/template/parse"
|
||||
)
|
||||
|
||||
type ReportTemplate string
|
||||
@@ -71,65 +31,112 @@ func (temp *ReportTemplate) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
func (temp ReportTemplate) Validate() error {
|
||||
template := string(temp)
|
||||
templateStr := string(temp)
|
||||
|
||||
for _, required := range requiredStructure {
|
||||
if !strings.Contains(template, required) {
|
||||
return fmt.Errorf("missing required HTML element: %s", required)
|
||||
// Validate template syntax using Go's template parser
|
||||
tmpl := template.New("validate").Funcs(template.FuncMap{
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"formatTime": func(t interface{}) string { return "" },
|
||||
"formatValue": func(v interface{}) string { return "" },
|
||||
})
|
||||
|
||||
parsed, err := tmpl.Parse(templateStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template syntax error: %w", err)
|
||||
}
|
||||
|
||||
var hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd bool
|
||||
// Validate essential fields are present using template parsing
|
||||
if err := validateEssentialFields(parsed.Tree.Root, &hasTitle, &hasRange, &hasFormatTime, &hasFormatValue, &hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !hasTitle {
|
||||
return fmt.Errorf("missing essential template field: {{$.Title}}")
|
||||
}
|
||||
if !hasRange {
|
||||
return fmt.Errorf("missing essential template field: {{range .Messages}}")
|
||||
}
|
||||
if !hasFormatTime {
|
||||
return fmt.Errorf("missing essential template field: {{formatTime .Time}}")
|
||||
}
|
||||
if !hasFormatValue {
|
||||
return fmt.Errorf("missing essential template field: {{formatValue .}}")
|
||||
}
|
||||
if !hasEnd {
|
||||
return fmt.Errorf("missing essential template field: {{end}}")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEssentialFields(node parse.Node, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd *bool) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *parse.ListNode:
|
||||
for _, sub := range n.Nodes {
|
||||
if err := validateEssentialFields(sub, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleaned := strings.TrimSpace(template)
|
||||
commentPattern := regexp.MustCompile(`^(?s:<!--.*?-->\s*)*`)
|
||||
cleaned = commentPattern.ReplaceAllString(cleaned, "")
|
||||
|
||||
if !strings.HasPrefix(cleaned, "<!DOCTYPE html>") {
|
||||
return fmt.Errorf("template must start with <!DOCTYPE html>")
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(template, field) {
|
||||
return fmt.Errorf("missing required template field: %s", field)
|
||||
case *parse.ActionNode:
|
||||
if n.Pipe != nil {
|
||||
for _, cmd := range n.Pipe.Cmds {
|
||||
cmdStr := cmd.String()
|
||||
if cmdStr == "$.Title" {
|
||||
*hasTitle = true
|
||||
}
|
||||
if len(cmd.Args) > 0 {
|
||||
firstArg := cmd.Args[0].String()
|
||||
if firstArg == "formatTime" {
|
||||
*hasFormatTime = true
|
||||
}
|
||||
if firstArg == "formatValue" {
|
||||
*hasFormatValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockStartPattern := regexp.MustCompile(`\{\{\s*(range|if|with)\b[^{}]*\}\}`)
|
||||
blockEndPattern := regexp.MustCompile(`\{\{\s*end\s*\}\}`)
|
||||
|
||||
blockStarts := blockStartPattern.FindAllString(template, -1)
|
||||
blockEnds := blockEndPattern.FindAllString(template, -1)
|
||||
|
||||
if len(blockStarts) != len(blockEnds) {
|
||||
return fmt.Errorf("unmatched template blocks: found %d block start(s) (range/if/with) and %d end(s)",
|
||||
len(blockStarts), len(blockEnds))
|
||||
}
|
||||
|
||||
for _, class := range requiredCSS {
|
||||
pattern := fmt.Sprintf(`\.%s\s*\{`, strings.TrimPrefix(class, "."))
|
||||
matched, _ := regexp.MatchString(pattern, template)
|
||||
if !matched {
|
||||
return fmt.Errorf("missing required CSS class: %s", class)
|
||||
case *parse.RangeNode:
|
||||
if n.Pipe != nil && len(n.Pipe.Cmds) > 0 {
|
||||
cmdStr := n.Pipe.Cmds[0].String()
|
||||
if cmdStr == ".Messages" {
|
||||
*hasRange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requiredTableElements := []string{
|
||||
"<table",
|
||||
"<thead>",
|
||||
"<tbody>",
|
||||
"<th",
|
||||
"<td",
|
||||
}
|
||||
|
||||
for _, element := range requiredTableElements {
|
||||
if !strings.Contains(template, element) {
|
||||
return fmt.Errorf("missing required table element: %s", element)
|
||||
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if n.ElseList != nil {
|
||||
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*hasEnd = true
|
||||
|
||||
expectedHeaders := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
|
||||
for _, header := range expectedHeaders {
|
||||
if !strings.Contains(template, header) {
|
||||
return fmt.Errorf("missing expected table header: %s", header)
|
||||
case *parse.IfNode:
|
||||
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.ElseList != nil {
|
||||
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case *parse.WithNode:
|
||||
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.ElseList != nil {
|
||||
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user