439
pkg/docgen/generator_test.go
Normal file
439
pkg/docgen/generator_test.go
Normal file
@@ -0,0 +1,439 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package docgen
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewGenerator(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
assert.NotNil(t, generator)
|
||||
assert.IsType(t, &Generator{}, generator)
|
||||
}
|
||||
|
||||
func TestGenerateHTML(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data DocumentData
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "basic document with all fields",
|
||||
data: DocumentData{
|
||||
Title: "Test Document",
|
||||
Content: "# Main Title\n\nThis is **bold** text with *italic* formatting.",
|
||||
Version: 1,
|
||||
Classification: ClassificationPublic,
|
||||
Approver: "John Doe",
|
||||
Description: "Test document description",
|
||||
PublishedAt: &now,
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "Alice Smith",
|
||||
SignedAt: &now,
|
||||
State: coredata.DocumentVersionSignatureStateSigned,
|
||||
RequestedAt: now,
|
||||
RequestedBy: "Bob Johnson",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: []string{
|
||||
"Test Document",
|
||||
"<h1>Main Title</h1>",
|
||||
"<strong>bold</strong>",
|
||||
"<em>italic</em>",
|
||||
"<td>1</td>",
|
||||
"PUBLIC",
|
||||
"John Doe",
|
||||
"Test document description",
|
||||
"Alice Smith",
|
||||
"Bob Johnson",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "document with HTML characters that need escaping",
|
||||
data: DocumentData{
|
||||
Title: "Test & <Script> Title",
|
||||
Content: "Normal markdown content",
|
||||
Approver: "John <script>alert('xss')</script> Doe",
|
||||
Description: "Description with & symbols and <tags>",
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "Alice & <Bob>",
|
||||
RequestedBy: "Carol <script>",
|
||||
State: coredata.DocumentVersionSignatureStateRequested,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: []string{
|
||||
"Test &amp; &lt;Script&gt; Title",
|
||||
"John &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; Doe",
|
||||
"Description with &amp; symbols and &lt;tags&gt;",
|
||||
"Alice &amp; &lt;Bob&gt;",
|
||||
"Carol &lt;script&gt;",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"<script>alert('xss')</script>",
|
||||
"Test & <Script> Title",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "document with markdown content",
|
||||
data: DocumentData{
|
||||
Title: "Markdown Test",
|
||||
Content: "## Section 1\n\n- Item 1\n- Item 2\n\n**Bold text** and *italic text*\n\n```code block```",
|
||||
},
|
||||
wantContains: []string{
|
||||
"<h2>Section 1</h2>",
|
||||
"<ul>",
|
||||
"<li>Item 1</li>",
|
||||
"<li>Item 2</li>",
|
||||
"</ul>",
|
||||
"<strong>Bold text</strong>",
|
||||
"<em>italic text</em>",
|
||||
"<code>code block</code>",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "document with all classification types",
|
||||
data: DocumentData{
|
||||
Title: "Classification Test",
|
||||
Classification: ClassificationConfidential,
|
||||
},
|
||||
wantContains: []string{"CONFIDENTIAL"},
|
||||
},
|
||||
{
|
||||
name: "empty document",
|
||||
data: DocumentData{},
|
||||
wantContains: []string{
|
||||
"<!DOCTYPE html>",
|
||||
"<html",
|
||||
"</html>",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "document with multiple signatures in different states",
|
||||
data: DocumentData{
|
||||
Title: "Signatures Test",
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "Signer 1",
|
||||
SignedAt: &now,
|
||||
State: coredata.DocumentVersionSignatureStateSigned,
|
||||
RequestedAt: now,
|
||||
RequestedBy: "Requester 1",
|
||||
},
|
||||
{
|
||||
SignedBy: "Signer 2",
|
||||
State: coredata.DocumentVersionSignatureStateRequested,
|
||||
RequestedAt: now,
|
||||
RequestedBy: "Requester 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: []string{
|
||||
"Signer 1",
|
||||
"Signer 2",
|
||||
"Requester 1",
|
||||
"Requester 2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := generator.GenerateHTML(tt.data)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
|
||||
resultStr := string(result)
|
||||
|
||||
// Check that all expected content is present
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, resultStr, want, "Expected content not found: %s", want)
|
||||
}
|
||||
|
||||
// Check that unwanted content is not present
|
||||
for _, wantNot := range tt.wantNotContains {
|
||||
assert.NotContains(t, resultStr, wantNot, "Unwanted content found: %s", wantNot)
|
||||
}
|
||||
|
||||
// Basic HTML structure validation
|
||||
assert.Contains(t, resultStr, "<!DOCTYPE html>")
|
||||
assert.Contains(t, resultStr, "<html")
|
||||
assert.Contains(t, resultStr, "</html>")
|
||||
assert.Contains(t, resultStr, "<head>")
|
||||
assert.Contains(t, resultStr, "</head>")
|
||||
assert.Contains(t, resultStr, "<body>")
|
||||
assert.Contains(t, resultStr, "</body>")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTML_ErrorHandling(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
|
||||
// Test with data that should not cause errors
|
||||
data := DocumentData{
|
||||
Title: "Valid Document",
|
||||
Content: "Valid content",
|
||||
}
|
||||
|
||||
result, err := generator.GenerateHTML(data)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
|
||||
func TestTemplateFunctions(t *testing.T) {
|
||||
t.Run("now function", func(t *testing.T) {
|
||||
nowFunc := templateFuncs["now"].(func() time.Time)
|
||||
result := nowFunc()
|
||||
assert.True(t, time.Since(result) < time.Second)
|
||||
})
|
||||
|
||||
t.Run("eq function", func(t *testing.T) {
|
||||
eqFunc := templateFuncs["eq"].(func(string, string) bool)
|
||||
assert.True(t, eqFunc("test", "test"))
|
||||
assert.False(t, eqFunc("test", "other"))
|
||||
})
|
||||
|
||||
t.Run("lower function", func(t *testing.T) {
|
||||
lowerFunc := templateFuncs["lower"].(func(string) string)
|
||||
assert.Equal(t, "hello world", lowerFunc("HELLO WORLD"))
|
||||
assert.Equal(t, "test", lowerFunc("Test"))
|
||||
})
|
||||
|
||||
t.Run("classificationString function", func(t *testing.T) {
|
||||
classFunc := templateFuncs["classificationString"].(func(Classification) string)
|
||||
assert.Equal(t, "PUBLIC", classFunc(ClassificationPublic))
|
||||
assert.Equal(t, "CONFIDENTIAL", classFunc(ClassificationConfidential))
|
||||
})
|
||||
|
||||
t.Run("formatContent function", func(t *testing.T) {
|
||||
formatFunc := templateFuncs["formatContent"].(func(string) template.HTML)
|
||||
|
||||
// Test markdown conversion
|
||||
result := formatFunc("**bold** text")
|
||||
assert.Contains(t, string(result), "<strong>bold</strong>")
|
||||
|
||||
// Test basic text
|
||||
result = formatFunc("simple text")
|
||||
assert.Contains(t, string(result), "<p>simple text</p>")
|
||||
|
||||
// Test empty content - goldmark produces empty output for empty input
|
||||
result = formatFunc("")
|
||||
// Empty content should produce empty result from goldmark
|
||||
assert.Equal(t, template.HTML(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClassificationConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
classification Classification
|
||||
expected string
|
||||
}{
|
||||
{"public", ClassificationPublic, "PUBLIC"},
|
||||
{"internal", ClassificationInternal, "INTERNAL"},
|
||||
{"confidential", ClassificationConfidential, "CONFIDENTIAL"},
|
||||
{"secret", ClassificationSecret, "SECRET"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, string(tt.classification))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLEscaping(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
|
||||
dangerousData := DocumentData{
|
||||
Title: "<script>alert('xss')</script>",
|
||||
Approver: "User & <Company>",
|
||||
Description: "Text with 'quotes' & \"double quotes\"",
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "<malicious>tag",
|
||||
RequestedBy: "User & Company",
|
||||
State: coredata.DocumentVersionSignatureStateRequested,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := generator.GenerateHTML(dangerousData)
|
||||
require.NoError(t, err)
|
||||
|
||||
resultStr := string(result)
|
||||
|
||||
// Verify dangerous content is escaped
|
||||
assert.NotContains(t, resultStr, "<script>alert('xss')</script>")
|
||||
assert.NotContains(t, resultStr, "<malicious>tag")
|
||||
assert.Contains(t, resultStr, "&lt;script&gt;")
|
||||
assert.Contains(t, resultStr, "&amp;")
|
||||
assert.Contains(t, resultStr, "&#39;")
|
||||
}
|
||||
|
||||
func TestMarkdownRendering(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "headers",
|
||||
markdown: "# H1\n## H2\n### H3",
|
||||
want: []string{"<h1>H1</h1>", "<h2>H2</h2>", "<h3>H3</h3>"},
|
||||
},
|
||||
{
|
||||
name: "emphasis",
|
||||
markdown: "**bold** and *italic*",
|
||||
want: []string{"<strong>bold</strong>", "<em>italic</em>"},
|
||||
},
|
||||
{
|
||||
name: "lists",
|
||||
markdown: "- Item 1\n- Item 2",
|
||||
want: []string{"<ul>", "<li>Item 1</li>", "<li>Item 2</li>", "</ul>"},
|
||||
},
|
||||
{
|
||||
name: "paragraphs",
|
||||
markdown: "Paragraph 1\n\nParagraph 2",
|
||||
want: []string{"<p>Paragraph 1</p>", "<p>Paragraph 2</p>"},
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
markdown: "`inline code` and\n```\ncode block\n```",
|
||||
want: []string{"<code>inline code</code>", "<pre><code>code block"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := DocumentData{
|
||||
Title: "Markdown Test",
|
||||
Content: tt.markdown,
|
||||
}
|
||||
|
||||
result, err := generator.GenerateHTML(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
resultStr := string(result)
|
||||
for _, want := range tt.want {
|
||||
assert.Contains(t, resultStr, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentVersionSignatureStates(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
now := time.Now()
|
||||
|
||||
states := []coredata.DocumentVersionSignatureState{
|
||||
coredata.DocumentVersionSignatureStateRequested,
|
||||
coredata.DocumentVersionSignatureStateSigned,
|
||||
// Add other states if they exist
|
||||
}
|
||||
|
||||
for _, state := range states {
|
||||
t.Run(string(state), func(t *testing.T) {
|
||||
data := DocumentData{
|
||||
Title: "State Test",
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "Test User",
|
||||
State: state,
|
||||
RequestedAt: now,
|
||||
RequestedBy: "Requester",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := generator.GenerateHTML(data)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargeContent(t *testing.T) {
|
||||
generator := NewGenerator()
|
||||
|
||||
// Create a large markdown content
|
||||
var largeContent strings.Builder
|
||||
for i := 0; i < 1000; i++ {
|
||||
largeContent.WriteString("# Section ")
|
||||
largeContent.WriteString(string(rune('A' + i%26)))
|
||||
largeContent.WriteString("\n\nThis is a paragraph with **bold** and *italic* text.\n\n")
|
||||
largeContent.WriteString("- List item 1\n- List item 2\n- List item 3\n\n")
|
||||
}
|
||||
|
||||
data := DocumentData{
|
||||
Title: "Large Document",
|
||||
Content: largeContent.String(),
|
||||
}
|
||||
|
||||
result, err := generator.GenerateHTML(data)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result)
|
||||
assert.True(t, len(result) > 10000) // Should be reasonably large
|
||||
}
|
||||
|
||||
func BenchmarkGenerateHTML(b *testing.B) {
|
||||
generator := NewGenerator()
|
||||
now := time.Now()
|
||||
|
||||
data := DocumentData{
|
||||
Title: "Benchmark Document",
|
||||
Content: "# Title\n\nThis is **bold** text with *italic* formatting.\n\n- Item 1\n- Item 2",
|
||||
Version: 1,
|
||||
Classification: ClassificationPublic,
|
||||
Approver: "John Doe",
|
||||
Description: "Benchmark test document",
|
||||
PublishedAt: &now,
|
||||
Signatures: []SignatureData{
|
||||
{
|
||||
SignedBy: "Alice Smith",
|
||||
SignedAt: &now,
|
||||
State: coredata.DocumentVersionSignatureStateSigned,
|
||||
RequestedAt: now,
|
||||
RequestedBy: "Bob Johnson",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := generator.GenerateHTML(data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user