372 lines
11 KiB
Go
372 lines
11 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"verstak/internal/core/actions"
|
||
"verstak/internal/core/activity"
|
||
"verstak/internal/core/files"
|
||
"verstak/internal/core/nodes"
|
||
"verstak/internal/core/notes"
|
||
"verstak/internal/core/plugins"
|
||
"verstak/internal/core/search"
|
||
"verstak/internal/core/storage"
|
||
syncsvc "verstak/internal/core/sync"
|
||
"verstak/internal/core/templates"
|
||
"verstak/internal/core/worklog"
|
||
)
|
||
|
||
// setupTestApp creates a full App with a temp vault directory for testing.
|
||
func setupTestApp(t *testing.T) (*App, string) {
|
||
t.Helper()
|
||
vaultRoot, err := os.MkdirTemp("", "verstak-test-*")
|
||
if err != nil {
|
||
t.Fatalf("mkdir temp: %v", err)
|
||
}
|
||
|
||
// Init vault structure
|
||
if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak"), 0o750); err != nil {
|
||
t.Fatalf("mkdir .verstak: %v", err)
|
||
}
|
||
if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak", "trash"), 0o750); err != nil {
|
||
t.Fatalf("mkdir trash: %v", err)
|
||
}
|
||
|
||
dbPath := filepath.Join(vaultRoot, ".verstak", "vault.db")
|
||
db, err := storage.Open(dbPath)
|
||
if err != nil {
|
||
t.Fatalf("open db: %v", err)
|
||
}
|
||
|
||
nodeRepo := nodes.NewRepository(db)
|
||
fileSvc := files.NewService(db, vaultRoot, nodeRepo)
|
||
noteSvc := notes.NewService(db, vaultRoot, nodeRepo, fileSvc)
|
||
actionSvc := actions.NewService(db)
|
||
activitySvc := activity.NewService(db)
|
||
worklogSvc := worklog.NewService(db)
|
||
searchSvc := search.NewService(db)
|
||
pm := plugins.NewManager(vaultRoot)
|
||
pm.Discover()
|
||
|
||
templatesReg := templates.NewRegistry()
|
||
if err := templatesReg.LoadSystem(); err != nil {
|
||
t.Fatalf("load templates: %v", err)
|
||
}
|
||
|
||
syncSvc := syncsvc.NewService(db, "test-device")
|
||
|
||
app := &App{
|
||
db: db,
|
||
nodes: nodeRepo,
|
||
files: fileSvc,
|
||
notes: noteSvc,
|
||
activity: activitySvc,
|
||
actions: actionSvc,
|
||
worklog: worklogSvc,
|
||
search: searchSvc,
|
||
plugins: pm,
|
||
sync: syncSvc,
|
||
templates: templatesReg,
|
||
vault: vaultRoot,
|
||
}
|
||
|
||
t.Cleanup(func() {
|
||
db.Close()
|
||
os.RemoveAll(vaultRoot)
|
||
})
|
||
|
||
return app, vaultRoot
|
||
}
|
||
|
||
func TestVaultLayout_CreateProjectTree(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
// 1. Create root "Проекты" from folder template
|
||
proj, err := app.CreateNodeFromTemplate("", "Проекты", "folder.default")
|
||
if err != nil {
|
||
t.Fatalf("create Проекты: %v", err)
|
||
}
|
||
if proj.FsPath != "Проекты" {
|
||
t.Errorf("expected fs_path 'Проекты', got %q", proj.FsPath)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(vault, "Проекты")); os.IsNotExist(err) {
|
||
t.Error("expected folder 'Проекты' to exist on disk")
|
||
}
|
||
|
||
// 2. Create child "Рабочие" inside Проекты
|
||
work, err := app.CreateNodeFromTemplate(proj.ID, "Рабочие", "folder.default")
|
||
if err != nil {
|
||
t.Fatalf("create Рабочие: %v", err)
|
||
}
|
||
expectedWorkPath := "Проекты/Рабочие"
|
||
if work.FsPath != expectedWorkPath {
|
||
t.Errorf("expected fs_path %q, got %q", expectedWorkPath, work.FsPath)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(vault, expectedWorkPath)); os.IsNotExist(err) {
|
||
t.Error("expected folder 'Проекты/Рабочие' to exist on disk")
|
||
}
|
||
|
||
// 3. Create project "Разработка серверной" from project template
|
||
server, err := app.CreateNodeFromTemplate(work.ID, "Разработка серверной", "project.default")
|
||
if err != nil {
|
||
t.Fatalf("create Разработка серверной: %v", err)
|
||
}
|
||
expectedServerPath := "Проекты/Рабочие/Разработка серверной"
|
||
if server.FsPath != expectedServerPath {
|
||
t.Errorf("expected fs_path %q, got %q", expectedServerPath, server.FsPath)
|
||
}
|
||
serverFolder := filepath.Join(vault, expectedServerPath)
|
||
if _, err := os.Stat(serverFolder); os.IsNotExist(err) {
|
||
t.Error("expected project folder on disk")
|
||
}
|
||
|
||
// 4. Verify template created Overview.md
|
||
overviewPath := filepath.Join(serverFolder, "Overview.md")
|
||
if _, err := os.Stat(overviewPath); os.IsNotExist(err) {
|
||
t.Log("note: Overview.md from template not created (may not be implemented)")
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_CreateNoteInsideProject(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
proj, err := app.CreateNodeFromTemplate("", "Тестовый проект", "project.default")
|
||
if err != nil {
|
||
t.Fatalf("create project: %v", err)
|
||
}
|
||
|
||
// Create a note inside the project
|
||
noteNode, fileRec, err := app.notes.Create(proj.ID, "Моя заметка", "")
|
||
if err != nil {
|
||
t.Fatalf("create note: %v", err)
|
||
}
|
||
if noteNode == nil || fileRec == nil {
|
||
t.Fatal("expected non-nil node and file record")
|
||
}
|
||
|
||
// Verify the note .md file is inside the project folder
|
||
expectedPath := filepath.Join(vault, proj.FsPath, "Моя заметка.md")
|
||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||
// Try the safe-display-name variant
|
||
expectedPath2 := filepath.Join(vault, proj.FsPath, "Моя_заметка.md")
|
||
if _, err2 := os.Stat(expectedPath2); os.IsNotExist(err2) {
|
||
// Show what actually exists
|
||
entries, _ := os.ReadDir(filepath.Join(vault, proj.FsPath))
|
||
t.Errorf("expected note file in project folder, found: %v", listNames(entries))
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_CopyFileIntoProject(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
proj, err := app.CreateNodeFromTemplate("", "Проект", "folder.default")
|
||
if err != nil {
|
||
t.Fatalf("create project: %v", err)
|
||
}
|
||
|
||
// Create a temp source file
|
||
srcFile := filepath.Join(vault, "source.txt")
|
||
if err := os.WriteFile(srcFile, []byte("hello"), 0o640); err != nil {
|
||
t.Fatalf("write source: %v", err)
|
||
}
|
||
|
||
// Create a file node inside the project
|
||
fileNode, err := app.nodes.Create(&proj.ID, "file", "test.txt", 0, "", "")
|
||
if err != nil {
|
||
t.Fatalf("create file node: %v", err)
|
||
}
|
||
|
||
// Copy the file into vault (using parent's fs_path)
|
||
rec, err := app.files.CopyIntoVault(fileNode.ID, srcFile, proj.FsPath)
|
||
if err != nil {
|
||
t.Fatalf("copy into vault: %v", err)
|
||
}
|
||
|
||
// Verify file lands in project folder
|
||
expectedPath := filepath.Join(vault, proj.FsPath, "source.txt")
|
||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||
t.Errorf("expected file at %s, record path = %s", expectedPath, rec.Path)
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_RenameParentUpdatesDescendants(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
root, _ := app.CreateNodeFromTemplate("", "Root", "folder.default")
|
||
child, _ := app.CreateNodeFromTemplate(root.ID, "Child", "folder.default")
|
||
grandchild, _ := app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default")
|
||
|
||
// Rename root
|
||
if err := app.RenameNode(root.ID, "RenamedRoot"); err != nil {
|
||
t.Fatalf("rename root: %v", err)
|
||
}
|
||
|
||
// Verify child fs_path updated
|
||
childUpdated, _ := app.nodes.GetActive(child.ID)
|
||
expectedChildPath := "RenamedRoot/Child"
|
||
if childUpdated.FsPath != expectedChildPath {
|
||
t.Errorf("expected child fs_path %q, got %q", expectedChildPath, childUpdated.FsPath)
|
||
}
|
||
|
||
// Verify grandchild fs_path updated
|
||
gcUpdated, _ := app.nodes.GetActive(grandchild.ID)
|
||
expectedGCPath := "RenamedRoot/Child/Grandchild"
|
||
if gcUpdated.FsPath != expectedGCPath {
|
||
t.Errorf("expected grandchild fs_path %q, got %q", expectedGCPath, gcUpdated.FsPath)
|
||
}
|
||
|
||
// Verify physical folders
|
||
if _, err := os.Stat(filepath.Join(vault, expectedChildPath)); os.IsNotExist(err) {
|
||
t.Error("expected child folder on disk after rename")
|
||
}
|
||
if _, err := os.Stat(filepath.Join(vault, expectedGCPath)); os.IsNotExist(err) {
|
||
t.Error("expected grandchild folder on disk after rename")
|
||
}
|
||
|
||
// Verify old path no longer exists
|
||
if _, err := os.Stat(filepath.Join(vault, "Root")); !os.IsNotExist(err) {
|
||
t.Error("expected old root path to not exist")
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_MoveNode(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
folder1, _ := app.CreateNodeFromTemplate("", "Folder1", "folder.default")
|
||
folder2, _ := app.CreateNodeFromTemplate("", "Folder2", "folder.default")
|
||
child, _ := app.CreateNodeFromTemplate(folder1.ID, "Child", "folder.default")
|
||
|
||
// Move child from Folder1 to Folder2
|
||
if err := app.MoveNode(child.ID, folder2.ID); err != nil {
|
||
t.Fatalf("move node: %v", err)
|
||
}
|
||
|
||
moved, _ := app.nodes.GetActive(child.ID)
|
||
expectedPath := "Folder2/Child"
|
||
if moved.FsPath != expectedPath {
|
||
t.Errorf("expected fs_path %q, got %q", expectedPath, moved.FsPath)
|
||
}
|
||
|
||
if _, err := os.Stat(filepath.Join(vault, expectedPath)); os.IsNotExist(err) {
|
||
t.Error("expected child folder at new location on disk")
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_DeleteMovesToTrash(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
node, _ := app.CreateNodeFromTemplate("", "ToDelete", "folder.default")
|
||
nodePath := filepath.Join(vault, "ToDelete")
|
||
if _, err := os.Stat(nodePath); os.IsNotExist(err) {
|
||
t.Fatal("expected folder to exist before delete")
|
||
}
|
||
|
||
// Delete the node
|
||
if err := app.DeleteNode(node.ID); err != nil {
|
||
t.Fatalf("delete node: %v", err)
|
||
}
|
||
|
||
// Verify node folder is no longer in original location
|
||
if _, err := os.Stat(nodePath); !os.IsNotExist(err) {
|
||
t.Error("expected node folder to be removed from original location")
|
||
}
|
||
|
||
// Verify trash has the folder
|
||
trashDir := filepath.Join(vault, ".verstak", "trash")
|
||
entries, _ := os.ReadDir(trashDir)
|
||
found := false
|
||
for _, e := range entries {
|
||
if e.IsDir() && contains(e.Name(), "ToDelete") {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("expected deleted folder in trash, found: %v", listNames(entries))
|
||
}
|
||
|
||
// Verify node is soft-deleted
|
||
_, err := app.nodes.GetActive(node.ID)
|
||
if err == nil {
|
||
t.Error("expected node to be soft-deleted")
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_NameConflict(t *testing.T) {
|
||
app, vault := setupTestApp(t)
|
||
|
||
node1, err := app.CreateNodeFromTemplate("", "SameName", "folder.default")
|
||
if err != nil {
|
||
t.Fatalf("create first: %v", err)
|
||
}
|
||
if node1.FsPath != "SameName" {
|
||
t.Errorf("expected fs_path 'SameName', got %q", node1.FsPath)
|
||
}
|
||
|
||
node2, err := app.CreateNodeFromTemplate("", "SameName", "folder.default")
|
||
if err != nil {
|
||
t.Fatalf("create second: %v", err)
|
||
}
|
||
if node2.FsPath == "SameName" {
|
||
t.Errorf("expected unique fs_path for second node, got same %q", node2.FsPath)
|
||
}
|
||
if node2.FsPath == node1.FsPath {
|
||
t.Error("expected different fs_path for conflicting name")
|
||
}
|
||
|
||
// Both folders should exist on disk
|
||
if _, err := os.Stat(filepath.Join(vault, node1.FsPath)); os.IsNotExist(err) {
|
||
t.Errorf("expected first folder at %s", node1.FsPath)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(vault, node2.FsPath)); os.IsNotExist(err) {
|
||
t.Errorf("expected second folder at %s", node2.FsPath)
|
||
}
|
||
}
|
||
|
||
func TestVaultLayout_VaultCheck(t *testing.T) {
|
||
app, _ := setupTestApp(t)
|
||
|
||
// Create a healthy vault structure
|
||
app.CreateNodeFromTemplate("", "Healthy", "folder.default")
|
||
child, _ := app.CreateNodeFromTemplate("", "Child", "folder.default")
|
||
app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default")
|
||
|
||
// Run vault check
|
||
result, err := app.VaultCheck()
|
||
if err != nil {
|
||
t.Fatalf("vault check: %v", err)
|
||
}
|
||
if !result.Healthy {
|
||
t.Errorf("expected healthy vault, got errors: %v", result.Errors)
|
||
}
|
||
if result.TotalNodes == 0 {
|
||
t.Error("expected at least 1 node in check")
|
||
}
|
||
}
|
||
|
||
// --- helpers ---
|
||
|
||
func listNames(entries []os.DirEntry) []string {
|
||
var names []string
|
||
for _, e := range entries {
|
||
names = append(names, e.Name())
|
||
}
|
||
return names
|
||
}
|
||
|
||
func contains(s, substr string) bool {
|
||
return len(s) >= len(substr) && containsStr(s, substr)
|
||
}
|
||
|
||
func containsStr(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|