Files
MyGoNavi/internal/appdata/root_test.go
Syngnat d3909c7dff feat(saved-query): 支持独立 SQL 文件与自定义存储目录
- 将已存查询内容从 JSON 拆分为独立 .sql 文件,并采用可读名称和数字后缀处理冲突
- 自动迁移旧版内联 SQL 与摘要文件名,保留外部编辑并在失败时回滚
- 支持选择、迁移、打开及恢复默认存储目录,补充数据根切换串行保护
- 新增已存查询右键在文件夹中打开,并同步处理查询重命名后的文件路径
- 补齐 Wails、浏览器模拟、六语言文案及后端和前端回归测试
2026-07-24 11:29:04 +08:00

347 lines
12 KiB
Go

package appdata
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestResolveActiveRootDefaultsToLegacyGonaviDir(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
root, err := ResolveActiveRoot()
if err != nil {
t.Fatalf("ResolveActiveRoot returned error: %v", err)
}
expected := filepath.Join(homeDir, ".gonavi")
if root != expected {
t.Fatalf("expected default root %q, got %q", expected, root)
}
}
func TestSetActiveRootWritesBootstrapAndResolveUsesIt(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
customRoot := filepath.Join(t.TempDir(), "gonavi-data")
savedRoot, err := SetActiveRoot(customRoot)
if err != nil {
t.Fatalf("SetActiveRoot returned error: %v", err)
}
if savedRoot != customRoot {
t.Fatalf("expected saved root %q, got %q", customRoot, savedRoot)
}
if _, err := os.Stat(BootstrapPath()); err != nil {
t.Fatalf("expected bootstrap file to exist: %v", err)
}
resolvedRoot, err := ResolveActiveRoot()
if err != nil {
t.Fatalf("ResolveActiveRoot returned error: %v", err)
}
if resolvedRoot != customRoot {
t.Fatalf("expected custom root %q, got %q", customRoot, resolvedRoot)
}
}
func TestSetActiveRootResetToDefaultRemovesBootstrap(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
customRoot := filepath.Join(t.TempDir(), "gonavi-data")
if _, err := SetActiveRoot(customRoot); err != nil {
t.Fatalf("SetActiveRoot custom returned error: %v", err)
}
defaultRoot, err := SetActiveRoot("")
if err != nil {
t.Fatalf("SetActiveRoot default returned error: %v", err)
}
expectedDefault := filepath.Join(homeDir, ".gonavi")
if defaultRoot != expectedDefault {
t.Fatalf("expected default root %q, got %q", expectedDefault, defaultRoot)
}
if _, err := os.Stat(BootstrapPath()); !os.IsNotExist(err) {
t.Fatalf("expected bootstrap file to be removed, got err=%v", err)
}
}
func TestResolveActiveRootPrefersEnvOverride(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
customRoot := filepath.Join(t.TempDir(), "custom-root")
if _, err := SetActiveRoot(customRoot); err != nil {
t.Fatalf("SetActiveRoot returned error: %v", err)
}
overrideRoot := filepath.Join(t.TempDir(), "override-root")
t.Setenv(dataRootEnvName, overrideRoot)
resolvedRoot, err := ResolveActiveRoot()
if err != nil {
t.Fatalf("ResolveActiveRoot returned error: %v", err)
}
if resolvedRoot != overrideRoot {
t.Fatalf("expected env override root %q, got %q", overrideRoot, resolvedRoot)
}
}
func TestDataRootAndLogDirectoryPreserveEachOtherInBootstrap(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
customDataRoot := filepath.Join(t.TempDir(), "gonavi-data")
customLogDirectory := filepath.Join(t.TempDir(), "gonavi-logs")
if _, err := SetConfiguredLogDirectory(customLogDirectory); err != nil {
t.Fatalf("SetConfiguredLogDirectory returned error: %v", err)
}
if _, err := SetActiveRoot(customDataRoot); err != nil {
t.Fatalf("SetActiveRoot returned error: %v", err)
}
resolvedLogDirectory, err := ResolveConfiguredLogDirectory()
if err != nil {
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
}
if resolvedLogDirectory != customLogDirectory {
t.Fatalf("expected custom log directory %q, got %q", customLogDirectory, resolvedLogDirectory)
}
if _, err := SetActiveRoot(""); err != nil {
t.Fatalf("reset SetActiveRoot returned error: %v", err)
}
if _, err := os.Stat(BootstrapPath()); err != nil {
t.Fatalf("bootstrap should remain while log directory is customized: %v", err)
}
resolvedLogDirectory, err = ResolveConfiguredLogDirectory()
if err != nil || resolvedLogDirectory != customLogDirectory {
t.Fatalf("log directory after data-root reset = %q, %v", resolvedLogDirectory, err)
}
if _, err := SetActiveRoot(customDataRoot); err != nil {
t.Fatalf("restore custom data root returned error: %v", err)
}
if _, err := SetConfiguredLogDirectory(""); err != nil {
t.Fatalf("reset SetConfiguredLogDirectory returned error: %v", err)
}
resolvedRoot, err := ResolveActiveRoot()
if err != nil || resolvedRoot != customDataRoot {
t.Fatalf("data root after log reset = %q, %v", resolvedRoot, err)
}
if _, err := os.Stat(BootstrapPath()); err != nil {
t.Fatalf("bootstrap should remain while data root is customized: %v", err)
}
if _, err := SetActiveRoot(""); err != nil {
t.Fatalf("final SetActiveRoot reset returned error: %v", err)
}
if _, err := os.Stat(BootstrapPath()); !os.IsNotExist(err) {
t.Fatalf("bootstrap should be removed when both settings use defaults, got err=%v", err)
}
}
func TestDefaultSavedQueryDirectoryUsesActiveRoot(t *testing.T) {
activeRoot := filepath.Join(t.TempDir(), "gonavi-data")
want := filepath.Join(activeRoot, savedQueryDirectoryName)
if got := DefaultSavedQueryDirectory(activeRoot); got != want {
t.Fatalf("DefaultSavedQueryDirectory = %q, want %q", got, want)
}
}
func TestSavedQueryDirectoryFollowsDataRootOnlyWhileUsingDefault(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
firstRoot := filepath.Join(t.TempDir(), "first-root")
secondRoot := filepath.Join(t.TempDir(), "second-root")
if _, err := SetActiveRoot(firstRoot); err != nil {
t.Fatalf("SetActiveRoot first returned error: %v", err)
}
resolved, err := ResolveSavedQueryDirectory(firstRoot)
if err != nil || resolved != filepath.Join(firstRoot, savedQueryDirectoryName) {
t.Fatalf("first default saved query directory = %q, %v", resolved, err)
}
if _, err := SetActiveRoot(secondRoot); err != nil {
t.Fatalf("SetActiveRoot second returned error: %v", err)
}
resolved, err = ResolveSavedQueryDirectory(secondRoot)
if err != nil || resolved != filepath.Join(secondRoot, savedQueryDirectoryName) {
t.Fatalf("second default saved query directory = %q, %v", resolved, err)
}
customDirectory := filepath.Join(t.TempDir(), "custom-saved-queries")
if _, err := SetConfiguredSavedQueryDirectory(customDirectory); err != nil {
t.Fatalf("SetConfiguredSavedQueryDirectory returned error: %v", err)
}
if _, err := SetActiveRoot(firstRoot); err != nil {
t.Fatalf("restore SetActiveRoot first returned error: %v", err)
}
resolved, err = ResolveSavedQueryDirectory(firstRoot)
if err != nil || resolved != customDirectory {
t.Fatalf("custom saved query directory after data-root switch = %q, %v; want %q", resolved, err, customDirectory)
}
}
func TestSavedQueryDirectoryAndOtherSettingsPreserveEachOtherInBootstrap(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
customDataRoot := filepath.Join(t.TempDir(), "gonavi-data")
customLogDirectory := filepath.Join(t.TempDir(), "gonavi-logs")
customSavedQueryDirectory := filepath.Join(t.TempDir(), "saved-queries")
if _, err := SetActiveRoot(customDataRoot); err != nil {
t.Fatalf("SetActiveRoot returned error: %v", err)
}
if _, err := SetConfiguredLogDirectory(customLogDirectory); err != nil {
t.Fatalf("SetConfiguredLogDirectory returned error: %v", err)
}
if _, err := SetConfiguredSavedQueryDirectory(customSavedQueryDirectory); err != nil {
t.Fatalf("SetConfiguredSavedQueryDirectory returned error: %v", err)
}
resolvedSavedQueryDirectory, err := ResolveSavedQueryDirectory(customDataRoot)
if err != nil || resolvedSavedQueryDirectory != customSavedQueryDirectory {
t.Fatalf("saved query directory = %q, %v; want %q", resolvedSavedQueryDirectory, err, customSavedQueryDirectory)
}
if _, err := SetActiveRoot(""); err != nil {
t.Fatalf("reset SetActiveRoot returned error: %v", err)
}
if _, err := SetConfiguredLogDirectory(""); err != nil {
t.Fatalf("reset SetConfiguredLogDirectory returned error: %v", err)
}
if _, err := os.Stat(BootstrapPath()); err != nil {
t.Fatalf("bootstrap should remain while saved query directory is customized: %v", err)
}
resolvedSavedQueryDirectory, err = ResolveConfiguredSavedQueryDirectory()
if err != nil || resolvedSavedQueryDirectory != customSavedQueryDirectory {
t.Fatalf("configured saved query directory = %q, %v; want %q", resolvedSavedQueryDirectory, err, customSavedQueryDirectory)
}
if _, err := SetConfiguredSavedQueryDirectory(""); err != nil {
t.Fatalf("reset SetConfiguredSavedQueryDirectory returned error: %v", err)
}
if _, err := os.Stat(BootstrapPath()); !os.IsNotExist(err) {
t.Fatalf("bootstrap should be removed when all settings use defaults, got err=%v", err)
}
defaultSavedQueryDirectory := filepath.Join(homeDir, ".gonavi", savedQueryDirectoryName)
resolvedSavedQueryDirectory, err = ResolveSavedQueryDirectory("")
if err != nil || resolvedSavedQueryDirectory != defaultSavedQueryDirectory {
t.Fatalf("default saved query directory = %q, %v; want %q", resolvedSavedQueryDirectory, err, defaultSavedQueryDirectory)
}
}
func TestSetConfiguredSavedQueryDirectoryRejectsFilePathWithoutChangingConfig(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
blockingPath := filepath.Join(t.TempDir(), "not-a-directory")
if err := os.WriteFile(blockingPath, []byte("blocked"), 0o644); err != nil {
t.Fatalf("write blocking file: %v", err)
}
if _, err := SetConfiguredSavedQueryDirectory(blockingPath); err == nil {
t.Fatal("expected file path to be rejected as a saved query directory")
}
configured, err := ResolveConfiguredSavedQueryDirectory()
if err != nil {
t.Fatalf("ResolveConfiguredSavedQueryDirectory returned error: %v", err)
}
if configured != "" {
t.Fatalf("failed update changed configured saved query directory to %q", configured)
}
}
func TestSetConfiguredLogDirectoryRejectsFilePathWithoutChangingConfig(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
blockingPath := filepath.Join(t.TempDir(), "not-a-directory")
if err := os.WriteFile(blockingPath, []byte("blocked"), 0o644); err != nil {
t.Fatalf("write blocking file: %v", err)
}
if _, err := SetConfiguredLogDirectory(blockingPath); err == nil {
t.Fatal("expected file path to be rejected as a log directory")
}
configured, err := ResolveConfiguredLogDirectory()
if err != nil {
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
}
if configured != "" {
t.Fatalf("failed update changed configured log directory to %q", configured)
}
}
func TestSetConfiguredLogDirectoryRejectsDirectoryAtLogFilePathWithoutChangingConfig(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
targetDirectory := filepath.Join(t.TempDir(), "logs")
if err := os.MkdirAll(filepath.Join(targetDirectory, configuredLogFileName), 0o755); err != nil {
t.Fatalf("create blocking log-file directory: %v", err)
}
if _, err := SetConfiguredLogDirectory(targetDirectory); err == nil {
t.Fatal("expected a directory at the log file path to be rejected")
}
configured, err := ResolveConfiguredLogDirectory()
if err != nil {
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
}
if configured != "" {
t.Fatalf("failed update changed configured log directory to %q", configured)
}
}
func TestBootstrapFileLockSerializesAccess(t *testing.T) {
lockPath := filepath.Join(t.TempDir(), "storage_root.json.lock")
first, err := acquireBootstrapFileLock(lockPath)
if err != nil {
t.Fatalf("acquire first bootstrap lock: %v", err)
}
defer first.Close()
acquired := make(chan *bootstrapFileLock, 1)
errs := make(chan error, 1)
go func() {
second, err := acquireBootstrapFileLock(lockPath)
if err != nil {
errs <- err
return
}
acquired <- second
}()
select {
case second := <-acquired:
_ = second.Close()
t.Fatal("second bootstrap lock acquired before the first was released")
case err := <-errs:
t.Fatalf("acquire second bootstrap lock: %v", err)
case <-time.After(50 * time.Millisecond):
}
if err := first.Close(); err != nil {
t.Fatalf("release first bootstrap lock: %v", err)
}
select {
case second := <-acquired:
if err := second.Close(); err != nil {
t.Fatalf("release second bootstrap lock: %v", err)
}
case err := <-errs:
t.Fatalf("acquire second bootstrap lock after release: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("second bootstrap lock did not acquire after the first was released")
}
}