| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "strconv" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/relay" |
| 10 | ) |
| 11 | |
| 12 | func TestBrowseThreadReturnsNewestAuthorEditsOutsideCursor(t *testing.T) { |
| 13 | h := newRoomHarness(t) |
| 14 | h.must("alice" , event.KIND_CREATE_GROUP, [][]string{{"h" , "general" }}, "" ) |
| 15 | root := h.must("alice" , event.KIND_THREAD, [][]string{{"h" , "general" }}, "original root" ) |
| 16 | reply := h.must("bob" , event.KIND_THREAD_REPLY, [][]string{{"h" , "general" }, {"e" , root.ID}}, "original reply" ) |
| 17 | |
| 18 | cursor := collaborationCursor(collaborationItemFrom(reply)) |
| 19 | |
| 20 | publishAt := func(who string, createdAt int64, target event.Event, content string) event.Event { |
| 21 | e := h.sign(who, event.KIND_CONTENT_EDIT, [][]string{{"h" , "general" }, {"e" , target.ID}}, content) |
| 22 | e.CreatedAt = createdAt |
| 23 | if err := event.Sign(&e, h.secrets[who]); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if _, err := h.tenant.router.Publish(h.ctx, e, relay.Session{PubKeys: []string{h.keys[who]}, RelayURL: h.tenant.RelayURL()}); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | return e |
| 30 | } |
| 31 | old := publishAt("alice" , time.Now().Unix()+1, root, "root v1" ) |
| 32 | newest := publishAt("alice" , old.CreatedAt+1, root, "root v2" ) |
| 33 | replyEdit := publishAt("bob" , newest.CreatedAt+1, reply, "reply v1" ) |
| 34 | |
| 35 | page, err := h.browse("bob" , "browsethread" , map[string]any{"id" : "general" , "event" : root.ID, "cursor" : cursor, "limit" : 1}) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | edits, ok := page["edits" ].([]any) |
| 40 | if !ok { |
| 41 | t.Fatalf("edits = %#v" , page["edits" ]) |
| 42 | } |
| 43 | found := map[string]bool{} |
| 44 | for _, raw := range edits { |
| 45 | row := raw.(map[string]any) |
| 46 | found[row["id" ].(string)] = true |
| 47 | } |
| 48 | if !found[newest.ID] || found[replyEdit.ID] || found[old.ID] { |
| 49 | t.Fatalf("edits = %#v, want newest visible root only" , edits) |
| 50 | } |
| 51 | |
| 52 | forged := h.sign("alice" , event.KIND_CONTENT_EDIT, [][]string{{"h" , "general" }, {"e" , reply.ID}}, "wrong author" ) |
| 53 | // The target belongs to Bob, so the author filter must exclude this edit. |
| 54 | if _, err := h.tenant.router.Publish(h.ctx, forged, relay.Session{PubKeys: []string{h.keys["alice" ]}, RelayURL: h.tenant.RelayURL()}); err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | page, err = h.browse("bob" , "browsethread" , map[string]any{"id" : "general" , "event" : root.ID}) |
| 58 | if err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | found = map[string]bool{} |
| 62 | for _, raw := range page["edits" ].([]any) { |
| 63 | found[raw.(map[string]any)["id" ].(string)] = true |
| 64 | if raw.(map[string]any)["content" ] == "wrong author" { |
| 65 | t.Fatal("forged edit was returned" ) |
| 66 | } |
| 67 | } |
| 68 | if !found[replyEdit.ID] || !found[newest.ID] { |
| 69 | t.Fatalf("unpaged edits = %#v, want root and reply" , page["edits" ]) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestRoomEditsKeepIndependentTargetsAndUseLastReference(t *testing.T) { |
| 74 | h := newRoomHarness(t) |
| 75 | h.must("alice" , event.KIND_CREATE_GROUP, [][]string{{"h" , "general" }}, "" ) |
| 76 | first := h.must("alice" , event.KIND_CHAT, [][]string{{"h" , "general" }}, "first" ) |
| 77 | second := h.must("alice" , event.KIND_CHAT, [][]string{{"h" , "general" }}, "second" ) |
| 78 | wanted := h.must("alice" , event.KIND_CONTENT_EDIT, [][]string{{"h" , "general" }, {"e" , first.ID}}, "first replacement" ) |
| 79 | var latest event.Event |
| 80 | for i := 0; i < 105; i++ { |
| 81 | // The first reference is context; only the last reference is edited. |
| 82 | latest = signedEvent(t, h.secrets["alice" ], event.KIND_CONTENT_EDIT, first.CreatedAt+int64(i)+1, [][]string{{"h" , "general" }, {"e" , first.ID}, {"e" , second.ID}}, "second replacement " +strconv.Itoa(i)) |
| 83 | if err := publishAs(t, h.tenant, latest); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | } |
| 87 | edits, err := h.tenant.roomEdits(h.ctx, h.keys["alice" ], "general" , []event.Event{first, second}) |
| 88 | if err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | found := map[string]bool{} |
| 92 | for _, edit := range edits { |
| 93 | found[edit.ID] = true |
| 94 | } |
| 95 | if len(edits) != 2 || !found[wanted.ID] || !found[latest.ID] { |
| 96 | t.Fatalf("lost independent target edit: %+v" , edits) |
| 97 | } |
| 98 | } |