Files
MyGoNavi/internal/app/export_target_path_test.go
Syngnat c4f24a6245 🐛 fix(data-export): 自动补齐导出文件格式后缀
- 为 CSV、XLSX、JSON、Markdown、HTML 和 SQL 保存对话框绑定对应过滤器并统一补齐扩展名
- 覆盖单表、查询、批量 SQL、数据库、Schema 与 SQL 审计导出路径
- 防止补后缀后的目标绕过覆盖确认,并补充真实落盘、多语言与安全回归测试
2026-07-19 14:27:00 +08:00

169 lines
6.3 KiB
Go

package app
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
func TestNormalizeExportTargetPath(t *testing.T) {
root := t.TempDir()
tests := []struct {
name string
filePath string
format string
want string
}{
{name: "csv", filePath: filepath.Join(root, "report"), format: "csv", want: filepath.Join(root, "report.csv")},
{name: "xlsx", filePath: filepath.Join(root, "report"), format: "xlsx", want: filepath.Join(root, "report.xlsx")},
{name: "json", filePath: filepath.Join(root, "report"), format: "json", want: filepath.Join(root, "report.json")},
{name: "markdown", filePath: filepath.Join(root, "report"), format: "md", want: filepath.Join(root, "report.md")},
{name: "html", filePath: filepath.Join(root, "report"), format: "html", want: filepath.Join(root, "report.html")},
{name: "sql", filePath: filepath.Join(root, "report"), format: "sql", want: filepath.Join(root, "report.sql")},
{name: "keeps matching extension case", filePath: filepath.Join(root, "report.CSV"), format: "csv", want: filepath.Join(root, "report.CSV")},
{name: "appends instead of replacing another extension", filePath: filepath.Join(root, "report.txt"), format: "csv", want: filepath.Join(root, "report.txt.csv")},
{name: "trims path and format", filePath: " " + filepath.Join(root, "report") + " ", format: " XLSX ", want: filepath.Join(root, "report.xlsx")},
{name: "blank path", filePath: " ", format: "csv", want: ""},
{name: "ignores unsupported format", filePath: filepath.Join(root, "report"), format: `..\\exe`, want: filepath.Join(root, "report")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeExportTargetPath(tt.filePath, tt.format); got != tt.want {
t.Fatalf("normalizeExportTargetPath(%q, %q) = %q, want %q", tt.filePath, tt.format, got, tt.want)
}
})
}
}
func TestExportFileDialogFilters(t *testing.T) {
for _, format := range []string{"csv", "xlsx", "json", "md", "html", "sql"} {
t.Run(format, func(t *testing.T) {
filters := exportFileDialogFilters(format)
if len(filters) != 1 {
t.Fatalf("exportFileDialogFilters(%q) returned %d filters, want 1", format, len(filters))
}
if want := "*." + format; filters[0].Pattern != want {
t.Fatalf("filter pattern = %q, want %q", filters[0].Pattern, want)
}
if filters[0].DisplayName != strings.ToUpper(format) {
t.Fatalf("filter display name = %q, want %q", filters[0].DisplayName, strings.ToUpper(format))
}
})
}
if filters := exportFileDialogFilters("../../exe"); len(filters) != 0 {
t.Fatalf("unsupported format returned filters: %#v", filters)
}
}
func TestExportDataWithOptionsAppendsFormatExtension(t *testing.T) {
selectedPath := filepath.Join(t.TempDir(), "report")
app := &App{
saveFileDialog: func(_ context.Context, options runtime.SaveDialogOptions) (string, error) {
if options.DefaultFilename != "report.csv" {
t.Fatalf("default filename = %q, want report.csv", options.DefaultFilename)
}
if len(options.Filters) != 1 || options.Filters[0].Pattern != "*.csv" {
t.Fatalf("unexpected CSV filters: %#v", options.Filters)
}
return selectedPath, nil
},
}
result := app.ExportDataWithOptions(
[]map[string]interface{}{{"id": 1, "name": "Alice"}},
[]string{"id", "name"},
"report",
ExportFileOptions{Format: "csv"},
)
if !result.Success {
t.Fatalf("ExportDataWithOptions failed: %#v", result)
}
targetPath := selectedPath + ".csv"
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("read normalized export target %q: %v", targetPath, err)
}
if !strings.Contains(string(content), "id,name") || !strings.Contains(string(content), "1,Alice") {
t.Fatalf("unexpected CSV content: %q", string(content))
}
if _, err := os.Stat(selectedPath); !os.IsNotExist(err) {
t.Fatalf("extensionless export path should not exist, stat err=%v", err)
}
}
func TestExportDataWithOptionsDoesNotSilentlyOverwriteNormalizedTarget(t *testing.T) {
selectedPath := filepath.Join(t.TempDir(), "report")
targetPath := selectedPath + ".csv"
if err := os.WriteFile(targetPath, []byte("keep-existing-content"), 0o600); err != nil {
t.Fatalf("prepare existing export target: %v", err)
}
app := &App{
saveFileDialog: func(_ context.Context, _ runtime.SaveDialogOptions) (string, error) {
return selectedPath, nil
},
}
result := app.ExportDataWithOptions(
[]map[string]interface{}{{"id": 1}},
[]string{"id"},
"report",
ExportFileOptions{Format: "csv"},
)
if result.Success {
t.Fatalf("expected export to reject an unconfirmed normalized target overwrite, got %#v", result)
}
if !strings.Contains(result.Message, ".csv") || !strings.Contains(result.Message, targetPath) {
t.Fatalf("overwrite rejection should identify the normalized target, got %q", result.Message)
}
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("read existing export target: %v", err)
}
if string(content) != "keep-existing-content" {
t.Fatalf("existing export target was overwritten: %q", string(content))
}
}
func TestWriteExportedSQLFileByPathDoesNotSilentlyOverwriteNormalizedTarget(t *testing.T) {
selectedPath := filepath.Join(t.TempDir(), "query")
targetPath := selectedPath + ".sql"
if err := os.WriteFile(targetPath, []byte("select 'keep';\n"), 0o600); err != nil {
t.Fatalf("prepare existing SQL target: %v", err)
}
result := writeExportedSQLFileByPath(selectedPath, "select 'replace';\n")
if result.Success {
t.Fatalf("expected SQL export to reject an unconfirmed normalized target overwrite, got %#v", result)
}
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("read existing SQL target: %v", err)
}
if string(content) != "select 'keep';\n" {
t.Fatalf("existing SQL target was overwritten: %q", string(content))
}
}
func TestResolveExportTargetPathAllowsExplicitExistingTarget(t *testing.T) {
targetPath := filepath.Join(t.TempDir(), "report.csv")
if err := os.WriteFile(targetPath, []byte("confirmed"), 0o600); err != nil {
t.Fatalf("prepare explicit export target: %v", err)
}
resolved, err := resolveExportTargetPath(targetPath, "csv")
if err != nil {
t.Fatalf("explicit target should retain the save dialog overwrite confirmation: %v", err)
}
if resolved != targetPath {
t.Fatalf("resolved explicit target = %q, want %q", resolved, targetPath)
}
}