Files
docspell-cli/mount_test.go
T
Jan Bader d15b7d082e fix: return friendly paths for lazily mounted items
Fetch the uniquely identified item when mount metadata has not yet been
populated, so get-path does not fall back to the ID-based location.

💘 Generated with Crush

Assisted-by: Crush:openai/gpt-5.6-terra
2026-10-07 10:41:11 +02:00

360 lines
11 KiB
Go

package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"bazil.org/fuse"
)
func TestLazyMountFetchesMonthOnlyWhenRead(t *testing.T) {
var queries []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
queries = append(queries, request.Query)
items := []SearchItem{}
if len(queries) == 1 {
items = []SearchItem{{
ID: "item-1",
Name: "Invoice",
Date: 1720396800000,
Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf", Size: 1}},
}}
}
_ = json.NewEncoder(w).Encode(map[string]any{
"groups": []map[string]any{{"items": items}},
})
}))
defer server.Close()
root := buildLazyMountTree(NewDocspellClient(server.URL, ""), "state:confirmed", 100)
year, err := root.Lookup(context.Background(), "2024")
if err != nil {
t.Fatal(err)
}
month, err := year.(*lazyYearNode).Lookup(context.Background(), "07")
if err != nil {
t.Fatal(err)
}
if len(queries) != 0 {
t.Fatalf("expected no search before reading month, got %v", queries)
}
entries, err := month.(*lazyMonthNode).ReadDirAll(context.Background())
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Name != "Invoice.pdf" {
t.Fatalf("unexpected month entries: %#v", entries)
}
if len(queries) != 2 || queries[0] != "(& state:confirmed date:2024-07 )" || queries[1] != "(& state:confirmed date:2024-07 )" {
t.Fatalf("unexpected queries: %v", queries)
}
}
func TestLazyByIDShardFetchesMatchingPrefix(t *testing.T) {
var queries []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
queries = append(queries, request.Query)
items := []SearchItem{}
if len(queries) == 1 {
items = []SearchItem{{
ID: "it-item-1",
Name: "Invoice",
Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf", Size: 1}},
}}
}
_ = json.NewEncoder(w).Encode(map[string]any{
"groups": []map[string]any{{"items": items}},
})
}))
defer server.Close()
root := buildLazyMountTree(NewDocspellClient(server.URL, ""), "", 100)
byID, err := root.Lookup(context.Background(), "by-id")
if err != nil {
t.Fatal(err)
}
shard, err := byID.(*lazyByIDNode).Lookup(context.Background(), "it")
if err != nil {
t.Fatal(err)
}
entries, err := shard.(*lazyByIDShardNode).ReadDirAll(context.Background())
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Name != "it-item-1.pdf" {
t.Fatalf("unexpected shard entries: %#v", entries)
}
if _, err := shard.(*lazyByIDShardNode).Lookup(context.Background(), "it-item-1.pdf"); err != nil {
t.Fatal(err)
}
lookupAgain, err := byID.(*lazyByIDNode).Lookup(context.Background(), "it")
if err != nil {
t.Fatal(err)
}
if lookupAgain != shard {
t.Fatal("expected repeated shard lookup to return its cached node")
}
if _, err := lookupAgain.(*lazyByIDShardNode).Lookup(context.Background(), "it-item-1.pdf"); err != nil {
t.Fatal(err)
}
if len(queries) != 2 || queries[0] != "id:it*" || queries[1] != "id:it*" {
t.Fatalf("unexpected queries: %v", queries)
}
}
func TestLazyByIDListsEveryPrefixWithoutSearching(t *testing.T) {
root := buildLazyMountTree(nil, "", 100)
byID, err := root.Lookup(context.Background(), "by-id")
if err != nil {
t.Fatal(err)
}
entries, err := byID.(*lazyByIDNode).ReadDirAll(context.Background())
if err != nil {
t.Fatal(err)
}
wantCount := len(docspellIDCharacters) * len(docspellIDCharacters)
if len(entries) != wantCount {
t.Fatalf("expected %d ID prefixes, got %d", wantCount, len(entries))
}
found := false
for _, entry := range entries {
if entry.Name == "7j" {
found = entry.Type == fuse.DT_Dir
break
}
}
if !found {
t.Fatalf("expected 7j directory in %#v", entries)
}
if _, err := byID.(*lazyByIDNode).Lookup(context.Background(), "7J"); err == nil {
t.Fatal("expected uppercase ID prefix lookup to fail")
}
for _, prefix := range []string{"-_", "_o", ".@"} {
if _, err := byID.(*lazyByIDNode).Lookup(context.Background(), prefix); err != nil {
t.Fatalf("expected %q ID prefix lookup to succeed: %v", prefix, err)
}
}
}
func TestBuildMountTreeGroupsByFolderAndDate(t *testing.T) {
items := []SearchItem{{
ID: "item-1",
Name: "Invoice/July",
Date: 1720396800000,
Folder: &DocspellEntity{
Name: "Bills/Private",
},
Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf", Size: 123}},
}}
root := buildMountTree(context.Background(), &DocspellClient{}, items)
folder, ok := root.children["Bills_Private"].(*dirNode)
if !ok {
t.Fatalf("expected sanitized folder directory, got %#v", root.children)
}
year, ok := folder.children["2024"].(*dirNode)
if !ok {
t.Fatalf("expected year directory, got %#v", folder.children)
}
month, ok := year.children["07"].(*dirNode)
if !ok {
t.Fatalf("expected month directory, got %#v", year.children)
}
if _, ok := month.children["Invoice_July.pdf"].(*fileNode); !ok {
t.Fatalf("expected item-named attachment file, got %#v", month.children)
}
}
func TestItemIDFromURLOrID(t *testing.T) {
if got := itemIDFromURLOrID("https://docspell.example/app/item/item-123"); got != "item-123" {
t.Fatalf("expected item-123, got %q", got)
}
if got := itemIDFromURLOrID("item-456"); got != "item-456" {
t.Fatalf("expected raw id, got %q", got)
}
}
func TestItemIDFromLocalByIDPath(t *testing.T) {
mountPath := t.TempDir()
path := filepath.Join(mountPath, "by-id", "it", "item-1.pdf")
got, err := itemIDFromLocalPath(path, config{MountPath: mountPath})
if err != nil {
t.Fatal(err)
}
if got != "item-1" {
t.Fatalf("expected item-1, got %q", got)
}
}
func TestItemIDFromLocalFriendlyPathUsesMetadata(t *testing.T) {
mountPath := t.TempDir()
metadata := mountMetadata{Items: map[string]mountedItemMetadata{
"item-1": {FriendlyPath: "Bills/2024/07/Invoice.pdf"},
}}
data, err := json.Marshal(metadata)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(mountPath, mountMetadataFileName), data, 0644); err != nil {
t.Fatal(err)
}
got, err := itemIDFromLocalPath(filepath.Join(mountPath, "Bills", "2024", "07", "Invoice.pdf"), config{MountPath: mountPath})
if err != nil {
t.Fatal(err)
}
if got != "item-1" {
t.Fatalf("expected item-1, got %q", got)
}
}
func TestBuildMountTreeDeduplicatesAttachmentNames(t *testing.T) {
items := []SearchItem{{
ID: "item-1",
Name: "Invoice",
Attachments: []ItemAttachment{
{ID: "att-1", Name: "same.pdf"},
{ID: "att-2", Name: "same.pdf"},
},
}}
root := buildMountTree(context.Background(), &DocspellClient{}, items)
folder := root.children["No Folder"].(*dirNode)
date := folder.children["No Date"].(*dirNode)
if _, ok := date.children["Invoice - 01.pdf"]; !ok {
t.Fatalf("expected first unique name, got %#v", date.children)
}
if _, ok := date.children["Invoice - 02.pdf"]; !ok {
t.Fatalf("expected second unique name, got %#v", date.children)
}
}
func TestFileNodeUnknownSizeDoesNotAdvertiseZeroLength(t *testing.T) {
f := &fileNode{size: 0, knownSize: false}
var attr fuse.Attr
if err := f.Attr(context.Background(), &attr); err != nil {
t.Fatal(err)
}
if attr.Size != 0 {
t.Fatalf("expected absent size to remain zero, got %d", attr.Size)
}
if f.knownSize {
t.Fatal("expected unknown size before download")
}
}
func TestBuildMountTreeAddsByIDDirectory(t *testing.T) {
items := []SearchItem{{
ID: "item-1",
Name: "Invoice",
Attachments: []ItemAttachment{
{ID: "att-1", Name: "original.pdf"},
{ID: "att-2", Name: "extra.txt"},
{ID: "att-3", Name: "scan.pdf"},
},
}}
root := buildMountTree(context.Background(), &DocspellClient{}, items)
byID, ok := root.children["by-id"].(*dirNode)
if !ok {
t.Fatalf("expected by-id directory, got %#v", root.children)
}
shard, ok := byID.children["it"].(*dirNode)
if !ok {
t.Fatalf("expected by-id shard, got %#v", byID.children)
}
for _, name := range []string{"item-1.pdf", "item-1-2.txt", "item-1-3.pdf"} {
if _, ok := shard.children[name].(*fileNode); !ok {
t.Fatalf("expected %s in by-id shard, got %#v", name, shard.children)
}
}
}
func TestBuildMountTreeAddsMetadataLookup(t *testing.T) {
items := []SearchItem{{
ID: "item-1",
Name: "Invoice/July",
Date: 1720396800000,
Folder: &DocspellEntity{
Name: "Bills/Private",
},
Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf", Size: 123}},
}}
root := buildMountTree(context.Background(), &DocspellClient{}, items)
metadataNode, ok := root.children[mountMetadataFileName].(*staticFileNode)
if !ok {
t.Fatalf("expected metadata file, got %#v", root.children[mountMetadataFileName])
}
var metadata mountMetadata
if err := json.Unmarshal(metadataNode.data, &metadata); err != nil {
t.Fatal(err)
}
got := metadata.Items["item-1"]
if got.FriendlyPath != "Bills_Private/2024/07/Invoice_July.pdf" {
t.Fatalf("expected friendly metadata path, got %q", got.FriendlyPath)
}
if got.ByIDPath != "by-id/it/item-1.pdf" {
t.Fatalf("expected by-id metadata path, got %q", got.ByIDPath)
}
}
func TestFriendlyLocalPathForItemIDUsesMetadata(t *testing.T) {
mountPath := t.TempDir()
metadata := mountMetadata{Items: map[string]mountedItemMetadata{
"item-1": {FriendlyPath: "Bills/2024/07/Invoice.pdf"},
}}
data, err := json.Marshal(metadata)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(mountPath, mountMetadataFileName), data, 0644); err != nil {
t.Fatal(err)
}
got, err := friendlyLocalPathForItemID(mountPath, "item-1")
if err != nil {
t.Fatal(err)
}
want := filepath.Join(mountPath, "Bills", "2024", "07", "Invoice.pdf")
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestFriendlyLocalPathForSearchItemsUsesFriendlyPath(t *testing.T) {
mountPath := t.TempDir()
items := []SearchItem{{
ID: "EkXte7yy5Yw-Tb1coaAtAQY-7WL61AMHGLQ-56kX2TaRabG",
Name: "Invoice",
Date: 1720396800000,
Folder: &DocspellEntity{
Name: "Bills",
},
Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf"}},
}}
got := friendlyLocalPathForSearchItems(mountPath, itemIDFromURLOrID("https://docs.javil.eu/app/item/EkXte7yy5Yw-Tb1coaAtAQY-7WL61AMHGLQ-56kX2TaRabG"), items)
want := filepath.Join(mountPath, "Bills", "2024", "07", "Invoice.pdf")
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}