279 lines
7.5 KiB
Go
279 lines
7.5 KiB
Go
package core
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"verstak/internal/core/actions"
|
||
"verstak/internal/core/files"
|
||
"verstak/internal/core/nodes"
|
||
"verstak/internal/core/notes"
|
||
"verstak/internal/core/search"
|
||
"verstak/internal/core/storage"
|
||
"verstak/internal/core/vault"
|
||
"verstak/internal/core/worklog"
|
||
)
|
||
|
||
func openTestDB(t *testing.T) *storage.DB {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
db, err := storage.Open(filepath.Join(dir, "test.db"))
|
||
if err != nil {
|
||
t.Fatalf("open db: %v", err)
|
||
}
|
||
t.Cleanup(func() { db.Close() })
|
||
return db
|
||
}
|
||
|
||
func TestMVPSmoke(t *testing.T) {
|
||
// Create a test vault with a real filesystem area.
|
||
vaultDir := t.TempDir()
|
||
|
||
// 1. Init vault.
|
||
if err := vault.Init(vaultDir); err != nil {
|
||
t.Fatalf("vault init: %v", err)
|
||
}
|
||
|
||
db := openTestDB(t)
|
||
nodeRepo := nodes.NewRepository(db)
|
||
fileSvc := files.NewService(db, vaultDir, nodeRepo)
|
||
noteSvc := notes.NewService(db, vaultDir, nodeRepo, fileSvc)
|
||
actionSvc := actions.NewService(db)
|
||
worklogSvc := worklog.NewService(db)
|
||
searchSvc := search.NewService(db)
|
||
|
||
// 2. Create client case structure.
|
||
client, err := nodeRepo.Create(nil, nodes.TypeCase, "ООО Ромашка", 0, "", "")
|
||
if err != nil {
|
||
t.Fatalf("create client: %v", err)
|
||
}
|
||
project, err := nodeRepo.Create(&client.ID, nodes.TypeCase, "Сайт", 0, "", "")
|
||
if err != nil {
|
||
t.Fatalf("create project: %v", err)
|
||
}
|
||
|
||
// 3. Create a Markdown note.
|
||
noteNode, noteRec, err := noteSvc.Create(project.ID, "overview.md", "")
|
||
if err != nil {
|
||
t.Fatalf("create note: %v", err)
|
||
}
|
||
if noteNode.Title != "overview.md" {
|
||
t.Errorf("note title = %q, want overview.md", noteNode.Title)
|
||
}
|
||
|
||
// 4. Write note content.
|
||
content := "# Сайт ООО Ромашка\n\nWordPress/nginx. Иногда обновление."
|
||
if err := noteSvc.Save(noteNode.ID, content); err != nil {
|
||
t.Fatalf("save note: %v", err)
|
||
}
|
||
|
||
// 5. Read note back.
|
||
got, err := noteSvc.Read(noteNode.ID)
|
||
if err != nil {
|
||
t.Fatalf("read note: %v", err)
|
||
}
|
||
if got != content {
|
||
t.Errorf("note content mismatch: got %q, want %q", got, content)
|
||
}
|
||
|
||
// 6. Verify note file exists on disk.
|
||
noteFileRecs, _ := fileSvc.ListByNode(noteNode.ID)
|
||
notePath := ""
|
||
if len(noteFileRecs) > 0 {
|
||
notePath = noteFileRecs[0].Path
|
||
if _, err := os.Stat(notePath); os.IsNotExist(err) {
|
||
t.Errorf("note file not on disk: %s", notePath)
|
||
}
|
||
}
|
||
_ = noteRec
|
||
|
||
// 7. Add a dummy external file.
|
||
dummyPath := filepath.Join(vaultDir, "dogovor.docx")
|
||
if err := os.WriteFile(dummyPath, []byte("dummy contract data"), 0644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
created, err := fileSvc.AddPathCopy(project.ID, dummyPath)
|
||
if err != nil {
|
||
t.Fatalf("add file: %v", err)
|
||
}
|
||
if len(created) == 0 {
|
||
t.Fatal("no nodes created from file import")
|
||
}
|
||
|
||
// 8. Open the imported file (just verify it opens without error).
|
||
fileNode := created[0]
|
||
if fileNode.Type != nodes.TypeFile {
|
||
t.Errorf("expected file type, got %s", fileNode.Type)
|
||
}
|
||
fileRecs, err := fileSvc.ListByNode(fileNode.ID)
|
||
if err != nil || len(fileRecs) == 0 {
|
||
t.Fatalf("file record not found for imported file")
|
||
}
|
||
_ = fileSvc.Open(fileRecs[0].ID) // may fail in test (no display)
|
||
|
||
// 9. Verify the imported file node exists as a child of the project.
|
||
children, err := nodeRepo.ListChildren(project.ID, false)
|
||
if err != nil {
|
||
t.Fatalf("list children: %v", err)
|
||
}
|
||
hasImported := false
|
||
for _, c := range children {
|
||
if c.Type == nodes.TypeFile && c.Title == "dogovor.docx" {
|
||
hasImported = true
|
||
break
|
||
}
|
||
}
|
||
if !hasImported {
|
||
t.Error("imported file node not found under project")
|
||
}
|
||
|
||
// 10. Create actions.
|
||
urlAction, err := actionSvc.Create(project.ID, actions.KindOpenURL, "Открыть сайт", "", "", "https://example.com", nil, false, false)
|
||
if err != nil {
|
||
t.Fatalf("create url action: %v", err)
|
||
}
|
||
if urlAction.Kind != actions.KindOpenURL {
|
||
t.Errorf("action kind = %s, want %s", urlAction.Kind, actions.KindOpenURL)
|
||
}
|
||
|
||
folderAction, err := actionSvc.Create(project.ID, actions.KindOpenFolder, "Открыть папку", vaultDir, "", "", nil, false, false)
|
||
if err != nil {
|
||
t.Fatalf("create folder action: %v", err)
|
||
}
|
||
if folderAction.Kind != actions.KindOpenFolder {
|
||
t.Errorf("action kind = %s", folderAction.Kind)
|
||
}
|
||
|
||
// 11. List actions.
|
||
actionList, err := actionSvc.ListByNode(project.ID)
|
||
if err != nil {
|
||
t.Fatalf("list actions: %v", err)
|
||
}
|
||
if len(actionList) != 2 {
|
||
t.Errorf("expected 2 actions, got %d", len(actionList))
|
||
}
|
||
|
||
// 12. Run an action (open URL — should not error).
|
||
_, err = actionSvc.Run(urlAction.ID)
|
||
if err != nil {
|
||
t.Fatalf("run url action: %v", err)
|
||
}
|
||
|
||
// 13. Add worklog.
|
||
entry, err := worklogSvc.Add(project.ID, "Обновил витрину сайта, баннеры", "", 180, false, false)
|
||
if err != nil {
|
||
t.Fatalf("add worklog: %v", err)
|
||
}
|
||
if entry.Minutes == nil || *entry.Minutes != 180 {
|
||
t.Errorf("worklog minutes = %v, want 180", entry.Minutes)
|
||
}
|
||
|
||
// 14. List worklog.
|
||
worklogList, err := worklogSvc.ListByNode(project.ID)
|
||
if err != nil {
|
||
t.Fatalf("list worklog: %v", err)
|
||
}
|
||
if len(worklogList) != 1 {
|
||
t.Errorf("expected 1 worklog entry, got %d", len(worklogList))
|
||
}
|
||
|
||
// 15. Search for something (FTS5 may not be available — gracefully skip).
|
||
_ = searchSvc.Rebuild()
|
||
_ = searchSvc.Index(noteNode.ID, noteNode.Title, content, notePath, "", "note")
|
||
results, err := searchSvc.Search("WordPress")
|
||
if err != nil {
|
||
_ = err
|
||
}
|
||
if len(results) > 0 {
|
||
t.Logf("search found %d results for 'WordPress'", len(results))
|
||
} else {
|
||
t.Log("search returned no results (FTS5 may not be available)")
|
||
}
|
||
|
||
// 16. Verify section filtering.
|
||
roots, err := nodeRepo.ListRoots(false)
|
||
if err != nil {
|
||
t.Fatalf("list roots: %v", err)
|
||
}
|
||
found := false
|
||
for _, r := range roots {
|
||
if r.ID == client.ID {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("client not found in roots")
|
||
}
|
||
|
||
// 17. Soft delete node and verify.
|
||
if err := nodeRepo.SoftDelete(project.ID); err != nil {
|
||
t.Fatalf("soft delete project: %v", err)
|
||
}
|
||
_, err = nodeRepo.GetActive(project.ID)
|
||
if err == nil {
|
||
t.Errorf("expected error getting deleted node")
|
||
}
|
||
|
||
// 18. Reopen app (simulate by reconnecting to same DB).
|
||
db2, err := storage.Open(db.Path())
|
||
if err != nil {
|
||
t.Fatalf("reopen db: %v", err)
|
||
}
|
||
defer db2.Close()
|
||
repo2 := nodes.NewRepository(db2)
|
||
|
||
// 19. Verify data persists.
|
||
client2, err := repo2.GetActive(client.ID)
|
||
if err != nil {
|
||
t.Fatalf("get client after reopen: %v", err)
|
||
}
|
||
if client2.Title != "ООО Ромашка" {
|
||
t.Errorf("client title after reopen = %q", client2.Title)
|
||
}
|
||
|
||
// 20. Verify soft-deleted node stays soft-deleted.
|
||
allNodes, err := repo2.ListChildren(client.ID, true)
|
||
if err != nil {
|
||
t.Fatalf("list children after reopen: %v", err)
|
||
}
|
||
if len(allNodes) == 0 {
|
||
t.Error("no children found after reopen")
|
||
}
|
||
|
||
t.Log("MVP smoke test passed!")
|
||
}
|
||
|
||
func TestMVPSmoke_WorklogReport(t *testing.T) {
|
||
db := openTestDB(t)
|
||
nodeRepo := nodes.NewRepository(db)
|
||
worklogSvc := worklog.NewService(db)
|
||
|
||
n, err := nodeRepo.Create(nil, nodes.TypeCase, "Test", 0, "", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err = worklogSvc.Add(n.ID, "Task 1", "", 60, true, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err = worklogSvc.Add(n.ID, "Task 2", "", 30, false, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
report, err := worklogSvc.Report(n.ID)
|
||
if err != nil {
|
||
t.Fatalf("report: %v", err)
|
||
}
|
||
if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") {
|
||
t.Error("report missing entries")
|
||
}
|
||
if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") {
|
||
t.Error("report missing tasks")
|
||
}
|
||
t.Logf("report:\n%s", report)
|
||
}
|