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) } }