mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
- 为 CSV、XLSX、JSON、Markdown、HTML 和 SQL 保存对话框绑定对应过滤器并统一补齐扩展名 - 覆盖单表、查询、批量 SQL、数据库、Schema 与 SQL 审计导出路径 - 防止补后缀后的目标绕过覆盖确认,并补充真实落盘、多语言与安全回归测试
169 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|