vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 19m ago
4b8c9e1d
| 1 | package web |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "rodent.software/vole/metadata" |
| 7 | ) |
| 8 | |
| 9 | // The thread is derived from data a push can write, so the shapes that matter are |
| 10 | // the awkward ones: a reply to a reply, a reply to a deleted comment, a reply whose |
| 11 | // parent is gone, and a cycle nobody intended. |
| 12 | |
| 13 | func ids(views []commentView) []string { |
| 14 | out := make([]string, 0, len(views)) |
| 15 | for _, v := range views { |
| 16 | out = append(out, v.ID) |
| 17 | } |
| 18 | return out |
| 19 | } |
| 20 | |
| 21 | func TestThreadCommentsFlattensRepliesUnderTheRoot(t *testing.T) { |
| 22 | comments := []commentView{ |
| 23 | {Comment: metadata.Comment{ID: "03", Author: "c", ReplyTo: "02"}}, |
| 24 | {Comment: metadata.Comment{ID: "01", Author: "a"}}, |
| 25 | {Comment: metadata.Comment{ID: "02", Author: "b", ReplyTo: "01"}}, |
| 26 | } |
| 27 | thread := threadComments(comments) |
| 28 | if len(thread) != 1 || thread[0].ID != "01" { |
| 29 | t.Fatalf("roots = %v, want just 01", ids(thread)) |
| 30 | } |
| 31 | // A reply to a reply is still shown under the root, and says what it answers. |
| 32 | if got := ids(thread[0].Replies); len(got) != 2 || got[0] != "02" || got[1] != "03" { |
| 33 | t.Fatalf("replies = %v, want [02 03]", got) |
| 34 | } |
| 35 | if thread[0].Replies[1].ReplyToName != "b" { |
| 36 | t.Fatalf("ReplyToName = %q, want b", thread[0].Replies[1].ReplyToName) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestThreadCommentsKeepsTombstones(t *testing.T) { |
| 41 | comments := []commentView{ |
| 42 | {Comment: metadata.Comment{ID: "02", Author: "b", ReplyTo: "01"}}, |
| 43 | } |
| 44 | thread := threadComments(comments) |
| 45 | if len(thread) != 1 || thread[0].ID != "01" || !thread[0].Deleted { |
| 46 | t.Fatalf("missing parent did not become a tombstone: %+v", thread) |
| 47 | } |
| 48 | if len(thread[0].Replies) != 1 || !thread[0].Replies[0].ReplyToDeleted { |
| 49 | t.Fatalf("reply does not know its parent was deleted: %+v", thread[0].Replies) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestThreadCommentsSurvivesCycles(t *testing.T) { |
| 54 | // Two comments answering each other have no root. Neither may vanish, and the |
| 55 | // walk must terminate. |
| 56 | comments := []commentView{ |
| 57 | {Comment: metadata.Comment{ID: "01", ReplyTo: "02"}}, |
| 58 | {Comment: metadata.Comment{ID: "02", ReplyTo: "01"}}, |
| 59 | {Comment: metadata.Comment{ID: "03"}}, |
| 60 | } |
| 61 | thread := threadComments(comments) |
| 62 | seen := map[string]bool{} |
| 63 | for _, root := range thread { |
| 64 | seen[root.ID] = true |
| 65 | for _, reply := range root.Replies { |
| 66 | seen[reply.ID] = true |
| 67 | } |
| 68 | } |
| 69 | for _, id := range []string{"01", "02", "03"} { |
| 70 | if !seen[id] { |
| 71 | t.Fatalf("comment %s disappeared from the thread: %+v", id, thread) |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // A comment that answers itself is not a thread; it is a root. |
| 77 | func TestThreadCommentsTreatsASelfReplyAsARoot(t *testing.T) { |
| 78 | thread := threadComments([]commentView{{Comment: metadata.Comment{ID: "01", ReplyTo: "01"}}}) |
| 79 | if len(thread) != 1 || thread[0].ID != "01" || len(thread[0].Replies) != 0 { |
| 80 | t.Fatalf("self reply = %+v", thread) |
| 81 | } |
| 82 | } |
| 83 |