munin/internal/store/related_test.go
2026-10-19 10:13:56 +02:00

197 lines
5.5 KiB
Go

package store
import (
"context"
"errors"
"testing"
"time"
)
// write legt einen Eintrag an und bricht den Test bei einem Fehler ab.
func write(t *testing.T, s *Store, projectID, slug, content string) {
t.Helper()
if _, err := s.PutEntry(context.Background(), EntryWrite{
ProjectID: projectID, Slug: slug, Title: "Titel " + slug, Content: content,
Source: SourceHuman, Author: "test",
}); err != nil {
t.Fatalf("Eintrag %q: %v", slug, err)
}
}
// TestRelatedEntries prüft Nachbarn im Verweisgraphen:
//
// start → a → fern
// b → start
// start → fehlt (Verweis ohne Eintrag)
// c (unverknüpft)
func TestRelatedEntries(t *testing.T) {
s := testStore(t)
ctx := context.Background()
p := testProject(t, s)
write(t, s, p.ID, "start", "siehe [[a]] und [[fehlt]]")
write(t, s, p.ID, "a", "weiter bei [[fern]]")
write(t, s, p.ID, "b", "hängt an [[start]]")
write(t, s, p.ID, "fern", "Ende")
write(t, s, p.ID, "c", "allein")
related, err := s.RelatedEntries(ctx, p.ID, "start", 2, 20)
if err != nil {
t.Fatal(err)
}
found := make(map[string]RelatedEntry)
for _, v := range related {
found[v.Slug] = v
}
want := map[string]struct {
distance int
via string
relation string
}{
"a": {1, "start", RelationLinksTo},
"b": {1, "start", RelationLinkedBy},
"fern": {2, "a", RelationLinksTo},
}
for slug, e := range want {
v, ok := found[slug]
if !ok {
t.Errorf("%q fehlt", slug)
continue
}
if v.Distance != e.distance || v.Via != e.via || v.Relation != e.relation {
t.Errorf("%q: Abstand %d über %q (%s), erwartet %d über %q (%s)",
slug, v.Distance, v.Via, v.Relation, e.distance, e.via, e.relation)
}
if v.Title != "Titel "+slug || v.UpdatedAt.IsZero() {
t.Errorf("%q: Titel oder Änderungsdatum fehlen: %+v", slug, v)
}
}
for _, slug := range []string{"start", "fehlt", "c"} {
if _, ok := found[slug]; ok {
t.Errorf("%q darf nicht unter den verwandten Einträgen stehen", slug)
}
}
// Näheres vor Fernerem.
if related[len(related)-1].Slug != "fern" {
t.Errorf("die zweite Reihe steht nicht am Ende: %+v", related)
}
// Mit Abstand 1 nur die direkten Nachbarn.
direct, err := s.RelatedEntries(ctx, p.ID, "start", 1, 20)
if err != nil {
t.Fatal(err)
}
if len(direct) != 2 {
t.Errorf("Abstand 1 lieferte %d Einträge, erwartet 2: %+v", len(direct), direct)
}
// Die Obergrenze gilt.
limited, err := s.RelatedEntries(ctx, p.ID, "start", 2, 1)
if err != nil {
t.Fatal(err)
}
if len(limited) != 1 {
t.Errorf("Limit 1 lieferte %d Einträge", len(limited))
}
}
// TestRelatedEntriesSkipsTrash prüft, dass gelöschte Einträge weder als
// Nachbar erscheinen noch als Brücke in die zweite Reihe dienen.
func TestRelatedEntriesSkipsTrash(t *testing.T) {
s := testStore(t)
ctx := context.Background()
p := testProject(t, s)
write(t, s, p.ID, "start", "siehe [[weg]]")
write(t, s, p.ID, "weg", "siehe [[dahinter]]")
write(t, s, p.ID, "dahinter", "Ende")
if err := s.MoveEntryToTrash(ctx, p.ID, "weg", "test"); err != nil {
t.Fatal(err)
}
related, err := s.RelatedEntries(ctx, p.ID, "start", 2, 20)
if err != nil {
t.Fatal(err)
}
if len(related) != 0 {
t.Errorf("über einen gelöschten Eintrag wurde etwas gefunden: %+v", related)
}
// Ein Ausgangseintrag im Papierkorb gilt als nicht vorhanden.
if _, err := s.RelatedEntries(ctx, p.ID, "weg", 2, 20); !errors.Is(err, ErrNotFound) {
t.Errorf("Fehler = %v, erwartet ErrNotFound", err)
}
}
func TestRelatedEntriesUnknownSlug(t *testing.T) {
s := testStore(t)
p := testProject(t, s)
if _, err := s.RelatedEntries(context.Background(), p.ID, "gibtesnicht", 2, 20); !errors.Is(err, ErrNotFound) {
t.Errorf("Fehler = %v, erwartet ErrNotFound", err)
}
}
// TestEntryStale prüft die Grenze für veraltetes Wissen (AUSBAUSTUFEN.md 3.5).
func TestEntryStale(t *testing.T) {
now := time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC)
cases := []struct {
name string
changed time.Time
want bool
}{
{"gestern", now.AddDate(0, 0, -1), false},
{"vor elf Monaten", now.AddDate(0, -11, 0), false},
{"vor zwei Jahren", now.AddDate(-2, 0, 0), true},
{"ohne Datum", time.Time{}, false},
}
for _, f := range cases {
t.Run(f.name, func(t *testing.T) {
if got := (Entry{UpdatedAt: f.changed}).Stale(now); got != f.want {
t.Errorf("Stale = %v, erwartet %v", got, f.want)
}
})
}
}
// TestPutEntryKeepsImportedTimestamps prüft, dass vorgegebene Zeitstempel
// beim Anlegen übernommen und beim Ändern ignoriert werden.
func TestPutEntryKeepsImportedTimestamps(t *testing.T) {
s := testStore(t)
ctx := context.Background()
p := testProject(t, s)
old := time.Date(2023, 3, 11, 9, 0, 0, 0, time.UTC)
e, err := s.PutEntry(ctx, EntryWrite{
ProjectID: p.ID, Slug: "alt", Title: "Alt", Content: "x",
Source: SourceHuman, CreatedAt: old, UpdatedAt: old,
})
if err != nil {
t.Fatal(err)
}
read, _ := s.EntryBySlug(ctx, p.ID, "alt")
if !read.CreatedAt.Equal(old) || !read.UpdatedAt.Equal(old) || !e.UpdatedAt.Equal(old) {
t.Errorf("Zeitstempel nicht übernommen: angelegt %v, geändert %v", read.CreatedAt, read.UpdatedAt)
}
// Eine Änderung passiert jetzt, egal was mitgegeben wird.
if _, err := s.PutEntry(ctx, EntryWrite{
ProjectID: p.ID, Slug: "alt", Title: "Alt", Content: "neu",
Source: SourceHuman, UpdatedAt: old,
}); err != nil {
t.Fatal(err)
}
read, _ = s.EntryBySlug(ctx, p.ID, "alt")
if read.UpdatedAt.Equal(old) {
t.Error("beim Ändern wurde der vorgegebene Zeitstempel übernommen")
}
if !read.CreatedAt.Equal(old) {
t.Error("beim Ändern ging das Anlegedatum verloren")
}
}