package store import ( "errors" "os" "path/filepath" "strings" "testing" ) func newTestStore(t *testing.T) *Store { t.Helper() s, err := New(t.TempDir()) if err != nil { t.Fatalf("New: %v", err) } return s } func TestCreateExtractsTagsAndIgnoresHeadings(t *testing.T) { s := newTestStore(t) content := "# Título do post\n\nUso #Linux e #devops.\n#linux repetida, #Hardware\n" n, err := s.Create("Boas vindas", FormatMD, "infra", content, "lg", "") if err != nil { t.Fatalf("Create: %v", err) } if len(n.Tags) != 3 { t.Fatalf("esperava 3 tags (devops, hardware, linux), obtive %v", n.Tags) } for _, tag := range []string{"devops", "hardware", "linux"} { if !contains(n.Tags, tag) { t.Errorf("tag %q ausente em %v", tag, n.Tags) } } // cabeçalho "# Título" não deve virar tag ("Título" nem entraria, e "título" tem acento) for _, tag := range n.Tags { if strings.Contains(tag, "t") && strings.HasPrefix(tag, "tí") { t.Errorf("cabeçalho vazou como tag: %v", n.Tags) } } if n.Hash == "" || n.Bytes != len(content) { t.Errorf("hash/bytes errados: %q %d", n.Hash, n.Bytes) } } func contains(xs []string, x string) bool { for _, v := range xs { if v == x { return true } } return false } func TestUpdateCreatesRevisionOnlyOnContentChange(t *testing.T) { s := newTestStore(t) n, err := s.Create("nota", FormatTxt, "", "v1", "lg", "") if err != nil { t.Fatal(err) } h1, err := s.History(n.ID) if err != nil || len(h1) != 1 { t.Fatalf("história inicial: %v %v", len(h1), err) } // Só metadados: nenhuma revisão nova. if _, err := s.Update(n.ID, "renomeada", FormatTxt, "pasta", "v1", "lg", ""); err != nil { t.Fatal(err) } if h, _ := s.History(n.ID); len(h) != 1 { t.Fatalf("conteúdo inalterado não deveria criar revisão; história: %+v", h) } // Conteúdo novo: segunda revisão com parent correto. n2, err := s.Update(n.ID, "renomeada", FormatTxt, "pasta", "v2", "lg", "ajuste final") if err != nil { t.Fatal(err) } h2, err := s.History(n.ID) if err != nil || len(h2) != 2 { t.Fatalf("esperava 2 revisões, obtive %d (%v)", len(h2), err) } if h2[1].Parent != h1[0].Hash || h2[1].Hash != n2.Hash { t.Errorf("corrente de revisões quebrada: %+v", h2) } if h2[1].Message != "ajuste final" { t.Errorf("mensagem: %q", h2[1].Message) } content, _, err := s.Content(n.ID) if err != nil || content != "v2" { t.Fatalf("content=%q err=%v", content, err) } } func TestRestoreAppendsRevisionWithOldContent(t *testing.T) { s := newTestStore(t) n, _ := s.Create("r", FormatMD, "", "primeiro", "", "") if _, err := s.Update(n.ID, "r", FormatMD, "", "segundo", "", ""); err != nil { t.Fatal(err) } first, err := s.History(n.ID) if err != nil { t.Fatal(err) } restored, err := s.Restore(n.ID, first[0].Hash, "lg") if err != nil { t.Fatalf("Restore: %v", err) } if restored.Hash != first[0].Hash { t.Errorf("nota restaurada deveria apontar para o blob antigo: %+v", restored) } content, _, err := s.Content(n.ID) if err != nil || content != "primeiro" { t.Fatalf("pós-resturo content=%q err=%v", content, err) } h, _ := s.History(n.ID) if len(h) != 3 || !strings.HasPrefix(h[2].Message, "Restauração da versão") { t.Fatalf("história pós-resturo: %+v", h) } // Restaurar a versão já atual devolve ErrUnchanged. if _, err := s.Restore(n.ID, first[0].Hash, "lg"); !errors.Is(err, ErrUnchanged) { t.Errorf("esperava ErrUnchanged, obtive %v", err) } // Hash desconhecido devolve ErrRevision. if _, err := s.Restore(n.ID, strings.Repeat("f", 64), "lg"); !errors.Is(err, ErrRevision) { t.Errorf("esperava ErrRevision, obtive %v", err) } } func TestListFiltersAndSorts(t *testing.T) { s := newTestStore(t) a, _ := s.Create("Alpha linux", FormatMD, "dev", "conteúdo #linux", "", "") s.Update(a.ID, "Alpha linux", FormatMD, "dev", "conteúdo #linux editado", "", "") b, _ := s.Create("Beta", FormatTxt, "casa", "compras", "", "") c, _ := s.Create("Gamma docker", FormatHTML, "dev", "

#devops #linux

", "", "") all, err := s.List(Filter{}) if err != nil || len(all) != 3 { t.Fatalf("list tudo: %d notas, %v", len(all), err) } if all[0].ID != c.ID { // mais recente primeiro (gamma criada por último) t.Errorf("ordenação por UpdatedAt errada: %v", ids(all)) } byCat, _ := s.List(Filter{Category: "dev"}) if len(byCat) != 2 { t.Errorf("categoria dev: %+v", ids(byCat)) } byTag, _ := s.List(Filter{Tag: "#linux"}) if len(byTag) != 2 { // alpha (md) e gamma (html) contêm #linux t.Errorf("tag linux: %+v", ids(byTag)) } byQ, _ := s.List(Filter{Query: "compras"}) if len(byQ) != 1 || byQ[0].ID != b.ID { t.Errorf("busca em conteúdo 'compras': %+v", ids(byQ)) } tags, _ := s.Tags() if len(tags) != 2 || tags[0].Tag != "linux" || tags[0].Count != 2 { t.Errorf("agregação de tags: %+v", tags) } cats, _ := s.Categories() if len(cats) != 2 || cats[0].Category != "casa" || cats[1].Category != "dev" { t.Errorf("agregação de categorias: %+v", cats) } } func ids(ns []*Note) []string { var out []string for _, n := range ns { out = append(out, n.ID) } return out } func TestDeleteGCsOrphanBlobs(t *testing.T) { s := newTestStore(t) n, _ := s.Create("gc", FormatTxt, "", "conteúdo único para o blob", "", "") blob := filepath.Join(s.dir, "blobs", n.Hash[:2], n.Hash) if _, err := os.Stat(blob); err != nil { t.Fatalf("blob deveria existir: %v", err) } if err := s.Delete(n.ID); err != nil { t.Fatal(err) } if _, err := os.Stat(blob); !errors.Is(err, os.ErrNotExist) { t.Errorf("blob órfão não coletado") } if _, err := s.Get(n.ID); !errors.Is(err, ErrNotFound) { t.Errorf("Get pós-delete: %v", err) } if err := s.Delete(n.ID); !errors.Is(err, ErrNotFound) { t.Errorf("Delete duplo deveria dar ErrNotFound, obtive %v", err) } } func TestSharedBlobSurvivesDeleteOfOneNote(t *testing.T) { s := newTestStore(t) a, _ := s.Create("a", FormatTxt, "", "mesmo", "", "") b, _ := s.Create("b", FormatTxt, "", "mesmo", "", "") // mesmo hash if a.Hash != b.Hash { t.Fatal("blobs deveriam ser deduplicados por conteúdo") } if err := s.Delete(a.ID); err != nil { t.Fatal(err) } content, _, err := s.Content(b.ID) if err != nil || content != "mesmo" { t.Fatalf("nota sobrevivente perdeu conteúdo: %q %v", content, err) } } func TestInvalidFormatAndOversizeContent(t *testing.T) { s := newTestStore(t) if _, err := s.Create("x", "rtf", "", "", "", ""); !errors.Is(err, ErrInvalid) { t.Errorf("formato inválido: %v", err) } if _, err := s.Update("naoexiste1", "x", FormatTxt, "", "a", "", ""); !errors.Is(err, ErrNotFound) { t.Errorf("update em nota ausente: %v", err) } if _, err := s.Create("x", FormatTxt, "", strings.Repeat("a", MaxContentBytes+1), "", ""); !errors.Is(err, ErrTooLarge) { t.Errorf("conteúdo gigante: %v", err) } } func TestFilenameAndRevisionContent(t *testing.T) { s := newTestStore(t) n, _ := s.Create("minha/nota: ruim", FormatMD, "", "oi #t", "", "") if got := n.Filename(); got != "minhanota ruim.md" { t.Errorf("filename: %q", got) } rc, err := s.RevisionWithContent(n.ID, n.Hash) if err != nil || rc.Content != "oi #t" { t.Fatalf("RevisionWithContent: %+v %v", rc, err) } if _, err := s.RevisionWithContent(n.ID, strings.Repeat("0", 64)); !errors.Is(err, ErrRevision) { t.Errorf("hash inexistente: %v", err) } before, after, _, err := s.DiffBetween(n.ID, n.Hash) if err != nil || before != "" || after != "oi #t" { t.Fatalf("DiffBetween primeira revisão: %q %q %v", before, after, err) } }