PDF generation and downloads are working
This commit is contained in:
+2
-1
@@ -42,4 +42,5 @@ bamort
|
||||
maintenance/testdata/*
|
||||
testdata/*_data.db
|
||||
tmp/main
|
||||
uploads/*
|
||||
uploads/*
|
||||
xporttemp/*
|
||||
@@ -68,6 +68,10 @@ func main() {
|
||||
maintenance.RegisterRoutes(protected)
|
||||
importer.RegisterRoutes(protected)
|
||||
pdfrender.RegisterRoutes(protected)
|
||||
|
||||
// Register public routes (no authentication)
|
||||
pdfrender.RegisterPublicRoutes(r)
|
||||
|
||||
logger.Info("API-Routen erfolgreich registriert")
|
||||
|
||||
// Server starten
|
||||
|
||||
@@ -27,7 +27,8 @@ type Config struct {
|
||||
DevTesting string // "yes" or "no", used to determine if we are in a test environment
|
||||
|
||||
// PDF Templates
|
||||
TemplatesDir string // Directory where PDF templates are stored
|
||||
TemplatesDir string // Directory where PDF templates are stored
|
||||
ExportTempDir string // Directory for temporary PDF exports
|
||||
}
|
||||
|
||||
// Cfg ist die globale Konfigurationsvariable
|
||||
@@ -42,14 +43,15 @@ func init() {
|
||||
// defaultConfig gibt die Standard-Konfiguration zurück
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
ServerPort: "8180",
|
||||
DatabaseURL: "",
|
||||
DatabaseType: "mysql",
|
||||
DebugMode: false,
|
||||
LogLevel: "INFO",
|
||||
Environment: "production",
|
||||
DevTesting: "no", // Default to "no", can be overridden in tests
|
||||
TemplatesDir: "./templates", // Default templates directory
|
||||
ServerPort: "8180",
|
||||
DatabaseURL: "",
|
||||
DatabaseType: "mysql",
|
||||
DebugMode: false,
|
||||
LogLevel: "INFO",
|
||||
Environment: "production",
|
||||
DevTesting: "no", // Default to "no", can be overridden in tests
|
||||
TemplatesDir: "./templates", // Default templates directory
|
||||
ExportTempDir: "./xporttemp", // Default export temp directory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +122,11 @@ func LoadConfig() *Config {
|
||||
config.TemplatesDir = templatesDir
|
||||
}
|
||||
|
||||
// Export Temp Directory
|
||||
if exportTempDir := os.Getenv("EXPORT_TEMP_DIR"); exportTempDir != "" {
|
||||
config.ExportTempDir = exportTempDir
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n",
|
||||
config.Environment, config.DevTesting, config.DatabaseType)
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SanitizeFilename removes or replaces characters that are problematic in filenames
|
||||
func SanitizeFilename(name string) string {
|
||||
// Replace umlauts
|
||||
replacements := map[string]string{
|
||||
"ä": "ae", "Ä": "Ae",
|
||||
"ö": "oe", "Ö": "Oe",
|
||||
"ü": "ue", "Ü": "Ue",
|
||||
"ß": "ss",
|
||||
}
|
||||
|
||||
result := name
|
||||
for old, new := range replacements {
|
||||
result = strings.ReplaceAll(result, old, new)
|
||||
}
|
||||
|
||||
// Replace problematic characters with underscore
|
||||
reg := regexp.MustCompile(`[^a-zA-Z0-9_.-]+`)
|
||||
result = reg.ReplaceAllString(result, "_")
|
||||
|
||||
// Remove consecutive underscores
|
||||
reg = regexp.MustCompile(`_+`)
|
||||
result = reg.ReplaceAllString(result, "_")
|
||||
|
||||
// Trim underscores from start and end
|
||||
result = strings.Trim(result, "_")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GenerateExportFilename creates a filename from character name and timestamp
|
||||
func GenerateExportFilename(charName string, timestamp time.Time) string {
|
||||
sanitizedName := SanitizeFilename(charName)
|
||||
timeStr := timestamp.Format("20060102_150405")
|
||||
return sanitizedName + "_" + timeStr + ".pdf"
|
||||
}
|
||||
|
||||
// EnsureExportTempDir creates the export temp directory if it doesn't exist
|
||||
func EnsureExportTempDir(dir string) error {
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
// CleanupOldFiles removes files older than maxAge from the directory
|
||||
// Returns the number of files deleted
|
||||
func CleanupOldFiles(dir string, maxAge time.Duration) (int, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, entry.Name())
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.ModTime().Before(cutoff) {
|
||||
if err := os.Remove(filePath); err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setupTestEnvironment(t *testing.T) {
|
||||
original := os.Getenv("ENVIRONMENT")
|
||||
os.Setenv("ENVIRONMENT", "test")
|
||||
t.Cleanup(func() {
|
||||
if original != "" {
|
||||
os.Setenv("ENVIRONMENT", original)
|
||||
} else {
|
||||
os.Unsetenv("ENVIRONMENT")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Simple name",
|
||||
input: "Character",
|
||||
expected: "Character",
|
||||
},
|
||||
{
|
||||
name: "Name with spaces",
|
||||
input: "Fanjo Vetrani",
|
||||
expected: "Fanjo_Vetrani",
|
||||
},
|
||||
{
|
||||
name: "Name with umlauts",
|
||||
input: "Müller Ökonom",
|
||||
expected: "Mueller_Oekonom",
|
||||
},
|
||||
{
|
||||
name: "Name with special chars",
|
||||
input: "Test/Character\\Name:With*Special?Chars",
|
||||
expected: "Test_Character_Name_With_Special_Chars",
|
||||
},
|
||||
{
|
||||
name: "Multiple consecutive spaces",
|
||||
input: "Test Name",
|
||||
expected: "Test_Name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeFilename(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeFilename(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateExportFilename(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Test with a fixed timestamp
|
||||
timestamp := time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC)
|
||||
filename := GenerateExportFilename("Fanjo Vetrani", timestamp)
|
||||
|
||||
expectedPrefix := "Fanjo_Vetrani_20231225_143045"
|
||||
if filename != expectedPrefix+".pdf" {
|
||||
t.Errorf("GenerateExportFilename() = %q, want prefix %q", filename, expectedPrefix)
|
||||
}
|
||||
|
||||
// Test with special characters
|
||||
filename2 := GenerateExportFilename("Test/Char*Name", timestamp)
|
||||
expectedPrefix2 := "Test_Char_Name_20231225_143045"
|
||||
if filename2 != expectedPrefix2+".pdf" {
|
||||
t.Errorf("GenerateExportFilename() = %q, want prefix %q", filename2, expectedPrefix2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureExportTempDir(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Use a test-specific temp directory
|
||||
testDir := filepath.Join(os.TempDir(), "bamort_test_xporttemp")
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
// First call should create the directory
|
||||
err := EnsureExportTempDir(testDir)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureExportTempDir() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify directory exists
|
||||
info, err := os.Stat(testDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Directory not created: %v", err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
t.Error("Path exists but is not a directory")
|
||||
}
|
||||
|
||||
// Second call should succeed (directory already exists)
|
||||
err = EnsureExportTempDir(testDir)
|
||||
if err != nil {
|
||||
t.Errorf("EnsureExportTempDir() on existing directory error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupOldFiles(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Create test directory
|
||||
testDir := filepath.Join(os.TempDir(), "bamort_test_cleanup")
|
||||
os.RemoveAll(testDir)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
// Create test files with different ages
|
||||
now := time.Now()
|
||||
|
||||
// Old file (8 days old)
|
||||
oldFile := filepath.Join(testDir, "old_file.pdf")
|
||||
if err := os.WriteFile(oldFile, []byte("old"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create old file: %v", err)
|
||||
}
|
||||
oldTime := now.Add(-8 * 24 * time.Hour)
|
||||
if err := os.Chtimes(oldFile, oldTime, oldTime); err != nil {
|
||||
t.Fatalf("Failed to set old file time: %v", err)
|
||||
}
|
||||
|
||||
// Recent file (3 days old)
|
||||
recentFile := filepath.Join(testDir, "recent_file.pdf")
|
||||
if err := os.WriteFile(recentFile, []byte("recent"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create recent file: %v", err)
|
||||
}
|
||||
recentTime := now.Add(-3 * 24 * time.Hour)
|
||||
if err := os.Chtimes(recentFile, recentTime, recentTime); err != nil {
|
||||
t.Fatalf("Failed to set recent file time: %v", err)
|
||||
}
|
||||
|
||||
// New file (1 hour old)
|
||||
newFile := filepath.Join(testDir, "new_file.pdf")
|
||||
if err := os.WriteFile(newFile, []byte("new"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create new file: %v", err)
|
||||
}
|
||||
|
||||
// Cleanup files older than 7 days
|
||||
count, err := CleanupOldFiles(testDir, 7*24*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("CleanupOldFiles() error = %v", err)
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
t.Errorf("CleanupOldFiles() deleted %d files, want 1", count)
|
||||
}
|
||||
|
||||
// Verify old file is deleted
|
||||
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
|
||||
t.Error("Old file should be deleted")
|
||||
}
|
||||
|
||||
// Verify recent file still exists
|
||||
if _, err := os.Stat(recentFile); err != nil {
|
||||
t.Error("Recent file should still exist")
|
||||
}
|
||||
|
||||
// Verify new file still exists
|
||||
if _, err := os.Stat(newFile); err != nil {
|
||||
t.Error("New file should still exist")
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,12 @@ func ListTemplates(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// ExportCharacterToPDF exports a character to PDF
|
||||
// ExportCharacterToPDF exports a character to PDF and saves it to xporttemp directory
|
||||
// Query params:
|
||||
// - template: template ID to use (default: "Default_A4_Quer")
|
||||
// - showUserName: whether to show user name (default: false)
|
||||
//
|
||||
// Returns JSON with filename: {"filename": "CharacterName_20231225_143045.pdf"}
|
||||
func ExportCharacterToPDF(c *gin.Context) {
|
||||
// Get character ID
|
||||
charID := c.Param("id")
|
||||
@@ -120,47 +122,110 @@ func ExportCharacterToPDF(c *gin.Context) {
|
||||
}
|
||||
allPDFs = append(allPDFs, page4PDFs...)
|
||||
|
||||
// If only one PDF, return it directly
|
||||
// Merge PDFs if needed
|
||||
var finalPDF []byte
|
||||
if len(allPDFs) == 1 {
|
||||
c.Data(http.StatusOK, "application/pdf", allPDFs[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Merge multiple PDFs
|
||||
tmpDir := "/tmp/bamort_pdf_export"
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Save individual PDFs
|
||||
var filePaths []string
|
||||
for i, pdf := range allPDFs {
|
||||
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
|
||||
if err := os.WriteFile(filename, pdf, 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
|
||||
finalPDF = allPDFs[0]
|
||||
} else {
|
||||
// Merge multiple PDFs
|
||||
tmpDir := "/tmp/bamort_pdf_export"
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Save individual PDFs
|
||||
var filePaths []string
|
||||
for i, pdf := range allPDFs {
|
||||
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
|
||||
if err := os.WriteFile(filename, pdf, 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
|
||||
return
|
||||
}
|
||||
filePaths = append(filePaths, filename)
|
||||
}
|
||||
|
||||
// Merge PDFs
|
||||
combinedPath := fmt.Sprintf("%s/combined.pdf", tmpDir)
|
||||
if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to merge PDFs: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Read combined PDF
|
||||
finalPDF, err = os.ReadFile(combinedPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"})
|
||||
return
|
||||
}
|
||||
filePaths = append(filePaths, filename)
|
||||
}
|
||||
|
||||
// Merge PDFs
|
||||
combinedPath := fmt.Sprintf("%s/combined.pdf", tmpDir)
|
||||
if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to merge PDFs: " + err.Error()})
|
||||
// Ensure export temp directory exists
|
||||
if err := EnsureExportTempDir(config.Cfg.ExportTempDir); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export directory"})
|
||||
return
|
||||
}
|
||||
|
||||
// Read combined PDF
|
||||
combinedPDF, err := os.ReadFile(combinedPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"})
|
||||
// Generate filename
|
||||
filename := GenerateExportFilename(char.Name, time.Now())
|
||||
filePath := filepath.Join(config.Cfg.ExportTempDir, filename)
|
||||
|
||||
// Save PDF to file
|
||||
if err := os.WriteFile(filePath, finalPDF, 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save PDF file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Set response headers
|
||||
filename := fmt.Sprintf("%s_character_sheet.pdf", char.Name)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Data(http.StatusOK, "application/pdf", combinedPDF)
|
||||
// Return filename
|
||||
c.JSON(http.StatusOK, gin.H{"filename": filename})
|
||||
}
|
||||
|
||||
// GetPDFFile serves a PDF file from the xporttemp directory
|
||||
func GetPDFFile(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
if filename == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent path traversal attacks - only allow base filename
|
||||
if filepath.Base(filename) != filename {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid filename"})
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow .pdf files
|
||||
if filepath.Ext(filename) != ".pdf" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Only PDF files are allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Construct full path
|
||||
filePath := filepath.Join(config.Cfg.ExportTempDir, filename)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
// CleanupExportTemp removes PDF files older than 7 days from xporttemp directory
|
||||
func CleanupExportTemp(c *gin.Context) {
|
||||
// Clean up files older than 7 days
|
||||
maxAge := 7 * 24 * time.Hour
|
||||
count, err := CleanupOldFiles(config.Cfg.ExportTempDir, maxAge)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cleanup files: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"deleted": count,
|
||||
"message": fmt.Sprintf("Deleted %d files older than 7 days", count),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -15,6 +19,7 @@ import (
|
||||
func init() {
|
||||
// Set templates directory for tests (tests run from pdfrender/ directory)
|
||||
config.Cfg.TemplatesDir = "../templates"
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}
|
||||
|
||||
func TestListTemplates(t *testing.T) {
|
||||
@@ -59,6 +64,14 @@ func TestExportCharacterToPDF(t *testing.T) {
|
||||
// Arrange
|
||||
database.SetupTestDB()
|
||||
|
||||
// Use test-specific temp directory
|
||||
testDir := "../xporttemp_test_basic"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
// Load test character
|
||||
char := &models.Char{}
|
||||
err := char.FirstID("18")
|
||||
@@ -77,24 +90,41 @@ func TestExportCharacterToPDF(t *testing.T) {
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Check content type
|
||||
// Check content type - now returns JSON
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
if contentType != "application/pdf" {
|
||||
t.Errorf("Expected Content-Type 'application/pdf', got '%s'", contentType)
|
||||
if !strings.Contains(contentType, "application/json") {
|
||||
t.Errorf("Expected Content-Type 'application/json', got '%s'", contentType)
|
||||
}
|
||||
|
||||
// Check PDF content
|
||||
body := w.Body.Bytes()
|
||||
if len(body) == 0 {
|
||||
t.Error("Expected non-empty PDF content")
|
||||
// Parse JSON response
|
||||
var response map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify filename is returned
|
||||
filename, ok := response["filename"]
|
||||
if !ok {
|
||||
t.Fatal("Response should contain 'filename' field")
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
t.Error("Filename should not be empty")
|
||||
}
|
||||
|
||||
// Verify PDF file exists
|
||||
filePath := filepath.Join(testDir, filename)
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read PDF file: %v", err)
|
||||
}
|
||||
|
||||
// Verify PDF marker
|
||||
if string(body[0:4]) != "%PDF" {
|
||||
t.Error("Response does not start with PDF marker")
|
||||
if string(data[0:4]) != "%PDF" {
|
||||
t.Error("File does not start with PDF marker")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +132,14 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
|
||||
// Arrange
|
||||
database.SetupTestDB()
|
||||
|
||||
// Use test-specific temp directory
|
||||
testDir := "../xporttemp_test_template"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/export/:id", ExportCharacterToPDF)
|
||||
@@ -113,13 +151,26 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
var response map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
filename := response["filename"]
|
||||
filePath := filepath.Join(testDir, filename)
|
||||
|
||||
// Verify it's a PDF
|
||||
body := w.Body.Bytes()
|
||||
if string(body[0:4]) != "%PDF" {
|
||||
t.Error("Response does not start with PDF marker")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read PDF: %v", err)
|
||||
}
|
||||
|
||||
if string(data[0:4]) != "%PDF" {
|
||||
t.Error("File does not start with PDF marker")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,3 +192,270 @@ func TestExportCharacterToPDF_CharacterNotFound(t *testing.T) {
|
||||
t.Errorf("Expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportCharacterToPDF_SavesFileAndReturnsFilename(t *testing.T) {
|
||||
// Arrange
|
||||
database.SetupTestDB()
|
||||
|
||||
// Use test-specific temp directory
|
||||
testDir := "../xporttemp_test"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
// Cleanup
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
// Load test character
|
||||
char := &models.Char{}
|
||||
err := char.FirstID("18")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load test character: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/export/:id", ExportCharacterToPDF)
|
||||
|
||||
// Act
|
||||
req, _ := http.NewRequest("GET", "/export/18", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var response map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Failed to parse JSON response: %v", err)
|
||||
}
|
||||
|
||||
// Check that filename is returned
|
||||
filename, ok := response["filename"]
|
||||
if !ok {
|
||||
t.Fatal("Response should contain 'filename' field")
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
t.Error("Filename should not be empty")
|
||||
}
|
||||
|
||||
// Verify filename format (should contain character name and timestamp)
|
||||
if !strings.Contains(filename, "Fanjo_Vetrani") {
|
||||
t.Errorf("Filename should contain sanitized character name, got: %s", filename)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(filename, ".pdf") {
|
||||
t.Errorf("Filename should end with .pdf, got: %s", filename)
|
||||
}
|
||||
|
||||
// Verify file exists in xporttemp directory
|
||||
filePath := filepath.Join(testDir, filename)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
t.Errorf("PDF file should exist at %s", filePath)
|
||||
}
|
||||
|
||||
// Verify it's a valid PDF
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read PDF file: %v", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
t.Error("PDF file should not be empty")
|
||||
}
|
||||
|
||||
if string(data[0:4]) != "%PDF" {
|
||||
t.Error("File should be a valid PDF")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPDFFile(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Create test directory and file
|
||||
testDir := "../xporttemp_test_get"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
// Create a test PDF file
|
||||
testFilename := "Test_Character_20231225_120000.pdf"
|
||||
testPDFContent := []byte("%PDF-1.4\nTest PDF Content")
|
||||
filePath := filepath.Join(testDir, testFilename)
|
||||
if err := os.WriteFile(filePath, testPDFContent, 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/file/:filename", GetPDFFile)
|
||||
|
||||
// Act - Get the PDF file
|
||||
req, _ := http.NewRequest("GET", "/file/"+testFilename, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Check content type
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
if contentType != "application/pdf" {
|
||||
t.Errorf("Expected Content-Type 'application/pdf', got '%s'", contentType)
|
||||
}
|
||||
|
||||
// Check content
|
||||
body := w.Body.Bytes()
|
||||
if string(body) != string(testPDFContent) {
|
||||
t.Errorf("Expected PDF content, got different content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPDFFile_NotFound(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
testDir := "../xporttemp_test_notfound"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/file/:filename", GetPDFFile)
|
||||
|
||||
// Act - Try to get non-existent file
|
||||
req, _ := http.NewRequest("GET", "/file/nonexistent.pdf", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPDFFile_PathTraversal(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
testDir := "../xporttemp_test_security"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/file/:filename", GetPDFFile)
|
||||
|
||||
// Act - Try path traversal attack
|
||||
req, _ := http.NewRequest("GET", "/file/../../../etc/passwd", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert - Should return error (either 404 or 400)
|
||||
if w.Code == http.StatusOK {
|
||||
t.Error("Should not allow path traversal attacks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupEndpoint(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Create test directory with old and new files
|
||||
testDir := "../xporttemp_test_cleanup_endpoint"
|
||||
config.Cfg.ExportTempDir = testDir
|
||||
defer func() {
|
||||
os.RemoveAll(testDir)
|
||||
config.Cfg.ExportTempDir = "../xporttemp"
|
||||
}()
|
||||
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Create old file (8 days old)
|
||||
oldFile := filepath.Join(testDir, "old_file.pdf")
|
||||
if err := os.WriteFile(oldFile, []byte("%PDF-old"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create old file: %v", err)
|
||||
}
|
||||
oldTime := now.Add(-8 * 24 * time.Hour)
|
||||
if err := os.Chtimes(oldFile, oldTime, oldTime); err != nil {
|
||||
t.Fatalf("Failed to set old file time: %v", err)
|
||||
}
|
||||
|
||||
// Create recent file (3 days old)
|
||||
recentFile := filepath.Join(testDir, "recent_file.pdf")
|
||||
if err := os.WriteFile(recentFile, []byte("%PDF-recent"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create recent file: %v", err)
|
||||
}
|
||||
recentTime := now.Add(-3 * 24 * time.Hour)
|
||||
if err := os.Chtimes(recentFile, recentTime, recentTime); err != nil {
|
||||
t.Fatalf("Failed to set recent file time: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/cleanup", CleanupExportTemp)
|
||||
|
||||
// Act
|
||||
req, _ := http.NewRequest("POST", "/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
// Check deleted count
|
||||
deletedCount, ok := response["deleted"]
|
||||
if !ok {
|
||||
t.Fatal("Response should contain 'deleted' field")
|
||||
}
|
||||
|
||||
// Should have deleted 1 file (the old one)
|
||||
if int(deletedCount.(float64)) != 1 {
|
||||
t.Errorf("Expected 1 file deleted, got %v", deletedCount)
|
||||
}
|
||||
|
||||
// Verify old file is deleted
|
||||
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
|
||||
t.Error("Old file should be deleted")
|
||||
}
|
||||
|
||||
// Verify recent file still exists
|
||||
if _, err := os.Stat(recentFile); err != nil {
|
||||
t.Error("Recent file should still exist")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,22 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterRoutes registers protected PDF routes
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
pdfGrp := r.Group("/pdf")
|
||||
|
||||
// List available templates
|
||||
// List available templates (protected)
|
||||
pdfGrp.GET("/templates", ListTemplates)
|
||||
|
||||
// Export character to PDF
|
||||
// Export character to PDF (protected)
|
||||
pdfGrp.GET("/export/:id", ExportCharacterToPDF)
|
||||
|
||||
// Cleanup old PDF files (protected)
|
||||
pdfGrp.POST("/cleanup", CleanupExportTemp)
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes registers public PDF routes (no authentication required)
|
||||
func RegisterPublicRoutes(r *gin.Engine) {
|
||||
// Get PDF file from xporttemp (public - for direct browser access)
|
||||
r.GET("/api/pdf/file/:filename", GetPDFFile)
|
||||
}
|
||||
|
||||
@@ -104,4 +104,5 @@
|
||||
|
||||
* currently the template fetched for rendering is set to Default_A4_Quer
|
||||
* remove inline css as far as possible
|
||||
* make pdf download popup an own view
|
||||
* make pdf download popup an own view
|
||||
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct n config.go
|
||||
Reference in New Issue
Block a user