1package daemon23import (4 "context"5 "encoding/json"6 "strings"7 "testing"89 "github.com/FelineStateMachine/tinyrelay/internal/event"10 "github.com/FelineStateMachine/tinyrelay/internal/storage"11)1213func TestBrowseDirectMessagesRequiresRecipientAndPaginates(t *testing.T) {14 _, tenant := testTenant(t)15 actorSecret := strings.Repeat("a", 63) + "1"16 otherSecret := strings.Repeat("b", 63) + "2"17 actor := signedEvent(t, actorSecret, event.KIND_WRAP, 100, [][]string{{"p", strings.Repeat("c", 64)}}, "ciphertext").Tags[0][1]18 recipient := actor19 first := signedEvent(t, otherSecret, event.KIND_WRAP, 200, [][]string{{"p", recipient}}, "first")20 second := signedEvent(t, otherSecret, event.KIND_WRAP, 200, [][]string{{"p", recipient}}, "second")21 third := signedEvent(t, otherSecret, event.KIND_WRAP, 199, [][]string{{"p", recipient}}, "third")22 for _, row := range []event.Event{first, second, third} {23 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {24 t.Fatal(err)25 }26 }27 value, err := tenant.Execute(context.Background(), recipient, "browsedirectmessages", []json.RawMessage{rawJSON(map[string]any{"limit": 2})})28 if err != nil {29 t.Fatal(err)30 }31 page := value.(map[string]any)32 events := page["events"].([]event.Event)33 if len(events) != 2 || events[0].ID != first.ID || events[1].ID != second.ID {34 t.Fatalf("first page = %#v", events)35 }36 next := page["next_cursor"].(string)37 value, err = tenant.Execute(context.Background(), recipient, "browsedirectmessages", []json.RawMessage{rawJSON(map[string]any{"limit": 2, "cursor": next})})38 if err != nil {39 t.Fatal(err)40 }41 page = value.(map[string]any)42 events = page["events"].([]event.Event)43 if len(events) != 1 || events[0].ID != third.ID || page["next_cursor"].(string) != "" {44 t.Fatalf("second page = %#v", page)45 }46 if events[0].Content != "third" {47 t.Fatalf("message content changed: %q", events[0].Content)48 }49}5051func TestBrowseDirectMessagesDoesNotUseOwnerBypass(t *testing.T) {52 _, tenant := testTenant(t)53 owner := tenant.Policy().Owner54 other := strings.Repeat("d", 64)55 row := signedEvent(t, strings.Repeat("e", 63)+"f", event.KIND_WRAP, 100, [][]string{{"p", other}}, "secret")56 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {57 t.Fatal(err)58 }59 if _, err := tenant.Execute(context.Background(), owner, "browsedirectmessages", []json.RawMessage{rawJSON(map[string]any{"limit": 10})}); err != nil {60 t.Fatal(err)61 }62 page := mustDirectMessagePage(t, tenant, owner)63 if len(page["events"].([]event.Event)) != 0 {64 t.Fatal("owner saw a message addressed to another user")65 }66}6768func TestBrowseDirectMessagesRequiresValidActor(t *testing.T) {69 _, tenant := testTenant(t)70 for _, actor := range []string{"", "not-a-pubkey"} {71 if _, err := tenant.Execute(context.Background(), actor, "browsedirectmessages", nil); err == nil || !strings.HasPrefix(err.Error(), "auth-required:") {72 t.Fatalf("actor %q error = %v", actor, err)73 }74 }75}7677func TestBrowseDirectMessagesFiltersRecipientAndKind(t *testing.T) {78 _, tenant := testTenant(t)79 actorSecret := strings.Repeat("1", 63) + "2"80 actor := signedEvent(t, actorSecret, event.KIND_WRAP, 99, nil, "").PubKey81 actorMessage := signedEvent(t, actorSecret, event.KIND_WRAP, 100, [][]string{{"p", actor}}, "recipient")82 otherRecipient := signedEvent(t, strings.Repeat("3", 63)+"4", event.KIND_WRAP, 101, [][]string{{"p", strings.Repeat("f", 64)}}, "other")83 wrongKind := signedEvent(t, strings.Repeat("5", 63)+"6", event.KIND_DM, 102, [][]string{{"p", actor}}, "legacy")84 for _, row := range []event.Event{actorMessage, otherRecipient, wrongKind} {85 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {86 t.Fatal(err)87 }88 }89 value, err := tenant.Execute(context.Background(), actor, "browsedirectmessages", nil)90 if err != nil {91 t.Fatal(err)92 }93 events := value.(map[string]any)["events"].([]event.Event)94 if len(events) != 1 || events[0].ID != actorMessage.ID {95 t.Fatalf("filtered events = %#v", events)96 }97}9899func TestBrowseDirectMessagesRejectsInvalidCursor(t *testing.T) {100 _, tenant := testTenant(t)101 actor := strings.Repeat("a", 64)102 _, err := tenant.Execute(context.Background(), actor, "browsedirectmessages", []json.RawMessage{rawJSON(map[string]any{"cursor": "broken"})})103 if err == nil || !strings.HasPrefix(err.Error(), "invalid: direct message cursor") {104 t.Fatalf("cursor error = %v", err)105 }106}107108func mustDirectMessagePage(t *testing.T, tenant *Tenant, actor string) map[string]any {109 t.Helper()110 value, err := tenant.Execute(context.Background(), actor, "browsedirectmessages", nil)111 if err != nil {112 t.Fatal(err)113 }114 return value.(map[string]any)115}