package relay import ( "os" "testing" "time" ) func newTestFeedMailbox(t *testing.T) *FeedMailbox { t.Helper() dir, err := os.MkdirTemp("", "dchain-feedtest-*") if err != nil { t.Fatalf("MkdirTemp: %v", err) } fm, err := OpenFeedMailbox(dir, 24*time.Hour) if err != nil { _ = os.RemoveAll(dir) t.Fatalf("OpenFeedMailbox: %v", err) } t.Cleanup(func() { _ = fm.Close() for i := 0; i < 20; i++ { if err := os.RemoveAll(dir); err == nil { return } time.Sleep(10 * time.Millisecond) } }) return fm } // TestFeedMailboxStoreAndGet: store round-trips content + metadata. func TestFeedMailboxStoreAndGet(t *testing.T) { fm := newTestFeedMailbox(t) post := &FeedPost{ PostID: "p1", Author: "authorhex", Content: "Hello #world from #dchain", } tags, err := fm.Store(post, 12345) if err != nil { t.Fatalf("Store: %v", err) } wantTags := []string{"world", "dchain"} if len(tags) != len(wantTags) { t.Fatalf("Store returned %v, want %v", tags, wantTags) } for i := range wantTags { if tags[i] != wantTags[i] { t.Errorf("Store tag[%d]: got %q, want %q", i, tags[i], wantTags[i]) } } got, err := fm.Get("p1") if err != nil || got == nil { t.Fatalf("Get: got %v err=%v", got, err) } if got.Content != post.Content { t.Errorf("content: got %q, want %q", got.Content, post.Content) } if got.CreatedAt != 12345 { t.Errorf("created_at: got %d, want 12345", got.CreatedAt) } if len(got.Hashtags) != 2 { t.Errorf("hashtags: got %v, want [world dchain]", got.Hashtags) } } // TestFeedMailboxTooLarge: rejects over-quota content. func TestFeedMailboxTooLarge(t *testing.T) { fm := newTestFeedMailbox(t) big := make([]byte, MaxPostBodySize+1) post := &FeedPost{ PostID: "big1", Author: "a", Attachment: big, } if _, err := fm.Store(post, 0); err != ErrPostTooLarge { t.Fatalf("Store huge post: got %v, want ErrPostTooLarge", err) } } // TestFeedMailboxHashtagIndex: hashtags are searchable + dedup + case-normalised. func TestFeedMailboxHashtagIndex(t *testing.T) { fm := newTestFeedMailbox(t) p1 := &FeedPost{PostID: "p1", Author: "a", Content: "post about #Go"} p2 := &FeedPost{PostID: "p2", Author: "b", Content: "Also #go programming"} p3 := &FeedPost{PostID: "p3", Author: "a", Content: "#Rust too"} if _, err := fm.Store(p1, 1000); err != nil { t.Fatal(err) } if _, err := fm.Store(p2, 2000); err != nil { t.Fatal(err) } if _, err := fm.Store(p3, 3000); err != nil { t.Fatal(err) } // #go is case-insensitive, should return both posts newest-first. ids, err := fm.PostsByHashtag("#Go", 10) if err != nil { t.Fatal(err) } if len(ids) != 2 || ids[0] != "p2" || ids[1] != "p1" { t.Errorf("PostsByHashtag(Go): got %v, want [p2 p1]", ids) } ids, _ = fm.PostsByHashtag("rust", 10) if len(ids) != 1 || ids[0] != "p3" { t.Errorf("PostsByHashtag(rust): got %v, want [p3]", ids) } } // TestFeedMailboxViewCounter: increments + reads. func TestFeedMailboxViewCounter(t *testing.T) { fm := newTestFeedMailbox(t) fm.Store(&FeedPost{PostID: "p", Author: "a", Content: "hi"}, 10) for i := 1; i <= 5; i++ { n, err := fm.IncrementView("p") if err != nil { t.Fatal(err) } if n != uint64(i) { t.Errorf("IncrementView #%d: got %d, want %d", i, n, i) } } if n, _ := fm.ViewCount("p"); n != 5 { t.Errorf("ViewCount: got %d, want 5", n) } } // TestFeedMailboxByAuthor: author chrono index returns newest first. func TestFeedMailboxByAuthor(t *testing.T) { fm := newTestFeedMailbox(t) fm.Store(&FeedPost{PostID: "old", Author: "a", Content: "one"}, 100) fm.Store(&FeedPost{PostID: "new", Author: "a", Content: "two"}, 500) fm.Store(&FeedPost{PostID: "mid", Author: "a", Content: "three"}, 300) fm.Store(&FeedPost{PostID: "other", Author: "b", Content: "four"}, 400) ids, err := fm.PostsByAuthor("a", 10) if err != nil { t.Fatal(err) } want := []string{"new", "mid", "old"} if len(ids) != len(want) { t.Fatalf("len: got %d, want %d (%v)", len(ids), len(want), ids) } for i := range want { if ids[i] != want[i] { t.Errorf("pos %d: got %q want %q", i, ids[i], want[i]) } } } // TestFeedMailboxDelete: removes body + indices. func TestFeedMailboxDelete(t *testing.T) { fm := newTestFeedMailbox(t) fm.Store(&FeedPost{PostID: "x", Author: "a", Content: "doomed #go"}, 100) if err := fm.Delete("x"); err != nil { t.Fatalf("Delete: %v", err) } if got, _ := fm.Get("x"); got != nil { t.Errorf("Get after delete: got %v, want nil", got) } if ids, _ := fm.PostsByHashtag("go", 10); len(ids) != 0 { t.Errorf("hashtag index: got %v, want []", ids) } if ids, _ := fm.PostsByAuthor("a", 10); len(ids) != 0 { t.Errorf("author index: got %v, want []", ids) } } // TestFeedMailboxRecentIDs: filters by window, sorts newest first. func TestFeedMailboxRecentIDs(t *testing.T) { fm := newTestFeedMailbox(t) now := time.Now().Unix() // p1 1 hour old, p2 5 hours old, p3 50 hours old. fm.Store(&FeedPost{PostID: "p1", Author: "a", Content: "a"}, now-3600) fm.Store(&FeedPost{PostID: "p2", Author: "b", Content: "b"}, now-5*3600) fm.Store(&FeedPost{PostID: "p3", Author: "c", Content: "c"}, now-50*3600) // 6-hour window: p1 and p2 only. ids, err := fm.RecentPostIDs(6*3600, 100) if err != nil { t.Fatal(err) } if len(ids) != 2 { t.Errorf("RecentPostIDs(6h): got %v, want 2 posts", ids) } // Newest first. if ids[0] != "p1" { t.Errorf("first post: got %s, want p1", ids[0]) } }