package export import ( "archive/zip" "errors" "os" "path/filepath" "strings" "testing" "time" "gopkg.in/yaml.v3" ) func TestExportImportProjectZip(t *testing.T) { db := openEventDB(t) seedEvent(t, db) uploads := t.TempDir() if err := os.WriteFile(filepath.Join(uploads, "logo.txt"), []byte("logo"), 0o644); err != nil { t.Fatalf("写 uploads: %v", err) } rules := filepath.Join(t.TempDir(), "rules.yaml") if err := os.WriteFile(rules, []byte("format: scoreboard-rules\nformatVersion: 1\n"), 0o644); err != nil { t.Fatalf("写 rules.yaml: %v", err) } zipPath := filepath.Join(t.TempDir(), "赛事包.zip") meta, err := ExportProjectZip(db, zipPath, ProjectExportOptions{ Name: "测试赛事", UploadsDir: uploads, RulesPath: rules, CreatedBy: "tester", Now: time.Date(2026, 9, 9, 12, 0, 0, 0, time.Local), }) if err != nil { t.Fatalf("导出赛事包: %v", err) } if meta.Format != projectFormat || meta.FormatVersion != projectFormatVersion { t.Fatalf("meta 格式 = %s/%d", meta.Format, meta.FormatVersion) } if meta.EventName != "测试运动会" { t.Errorf("meta.eventName = %q,期望 测试运动会", meta.EventName) } for _, want := range []string{"event.db", "uploads/logo.txt", "rules.yaml", "meta.yaml"} { if !zipHas(zipPath, t, want) { t.Errorf("包内缺少 %s", want) } } got, err := InspectProjectZip(zipPath) if err != nil { t.Fatalf("预览赛事包: %v", err) } if got.Name != "测试赛事" || got.Database.Path != "event.db" || got.Database.SHA256 == "" { t.Fatalf("预览结果异常: %+v", got) } root := t.TempDir() res, err := ImportProjectZip(zipPath, root, ImportOptions{}) if err != nil { t.Fatalf("导入赛事包: %v", err) } if res.Dir != filepath.Join(root, "测试赛事") { t.Errorf("导入目录 = %q", res.Dir) } if _, err := os.Stat(res.EventDB); err != nil { t.Fatalf("导入后 event.db 不存在: %v", err) } if _, err := os.Stat(filepath.Join(res.Dir, "uploads", "logo.txt")); err != nil { t.Errorf("导入后 uploads 资源缺失: %v", err) } assertTeams(t, res.EventDB, 2) if _, err := ImportProjectZip(zipPath, root, ImportOptions{}); !errors.Is(err, ErrProjectExists) { t.Fatalf("同名导入应报 ErrProjectExists,实际 %v", err) } if _, err := ImportProjectZip(zipPath, root, ImportOptions{Overwrite: true}); err != nil { t.Fatalf("覆盖导入: %v", err) } assertTeams(t, filepath.Join(root, "测试赛事", "event.db"), 2) renamed, err := ImportProjectZip(zipPath, root, ImportOptions{Rename: "另一场赛事"}) if err != nil { t.Fatalf("改名导入: %v", err) } if renamed.Name != "另一场赛事" { t.Errorf("改名导入结果 = %q", renamed.Name) } } func TestImportRejectsBadPackages(t *testing.T) { base := ProjectMeta{ Format: projectFormat, FormatVersion: projectFormatVersion, Name: "坏包", Database: ProjectFile{Path: "event.db", SHA256: strings.Repeat("0", 64)}, Files: []ProjectFile{{Path: "event.db", SHA256: strings.Repeat("0", 64)}}, } t.Run("校验和不匹配", func(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "bad.zip") writeZip(t, p, map[string]string{ "meta.yaml": marshalMeta(t, base), "event.db": "not a database", }) _, err := ImportProjectZip(p, t.TempDir(), ImportOptions{}) if !errors.Is(err, ErrChecksumMismatch) { t.Fatalf("期望 ErrChecksumMismatch,实际 %v", err) } }) t.Run("版本过新", func(t *testing.T) { meta := base meta.FormatVersion = 99 dir := t.TempDir() p := filepath.Join(dir, "bad.zip") writeZip(t, p, map[string]string{"meta.yaml": marshalMeta(t, meta), "event.db": "x"}) _, err := ImportProjectZip(p, t.TempDir(), ImportOptions{}) if !errors.Is(err, ErrUnsupportedVersion) { t.Fatalf("期望 ErrUnsupportedVersion,实际 %v", err) } }) t.Run("路径穿越", func(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "bad.zip") writeZip(t, p, map[string]string{ "meta.yaml": marshalMeta(t, base), "../evil.txt": "oops", "event.db": "x", }) _, err := ImportProjectZip(p, t.TempDir(), ImportOptions{}) if !errors.Is(err, ErrInvalidPackage) { t.Fatalf("期望 ErrInvalidPackage,实际 %v", err) } }) t.Run("缺 meta.yaml", func(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "bad.zip") writeZip(t, p, map[string]string{"event.db": "x"}) if _, err := InspectProjectZip(p); !errors.Is(err, ErrInvalidPackage) { t.Fatalf("期望 ErrInvalidPackage,实际 %v", err) } }) } func TestSafeProjectName(t *testing.T) { bad := []string{"", "..", "../evil", "a/b", `a\b`, "CON", "lpt1.db", "a:b", "x?y"} for _, name := range bad { if _, err := SafeProjectName(name); err == nil { t.Errorf("SafeProjectName(%q) 应报错", name) } } for _, name := range []string{"2026 运动会", "test-event_1", "测试赛事"} { if _, err := SafeProjectName(name); err != nil { t.Errorf("SafeProjectName(%q) 不应报错: %v", name, err) } } } func assertTeams(t *testing.T, path string, want int) { t.Helper() db := openSQLite(t, path) defer func() { _ = db.Close() }() var n int if err := db.QueryRow(`SELECT count(*) FROM teams`).Scan(&n); err != nil { t.Fatalf("查询 %s 的 teams: %v", path, err) } if n != want { t.Errorf("%s 的 teams 数 = %d,期望 %d", path, n, want) } } func zipHas(path string, t *testing.T, name string) bool { t.Helper() r, err := zip.OpenReader(path) if err != nil { t.Fatalf("打开 zip: %v", err) } defer func() { _ = r.Close() }() for _, f := range r.File { if f.Name == name { return true } } return false } func writeZip(t *testing.T, path string, files map[string]string) { t.Helper() f, err := os.Create(path) if err != nil { t.Fatalf("创建 zip: %v", err) } zw := zip.NewWriter(f) for name, body := range files { w, err := zw.Create(name) if err != nil { t.Fatalf("创建条目 %s: %v", name, err) } if _, err := w.Write([]byte(body)); err != nil { t.Fatalf("写入条目 %s: %v", name, err) } } if err := zw.Close(); err != nil { t.Fatalf("关闭 zip: %v", err) } if err := f.Close(); err != nil { t.Fatalf("关闭文件: %v", err) } } func marshalMeta(t *testing.T, meta ProjectMeta) string { t.Helper() raw, err := yaml.Marshal(&meta) if err != nil { t.Fatalf("序列化 meta: %v", err) } return string(raw) }