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