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.
|
c717d4eb
Keenan Nemetz
13d ago
|
1 | package web |
| 2 | ||
| 3 | import ( | |
| 4 | "testing" | |
| 5 | ||
|
d37d301f
Keenan Nemetz
15h ago
|
6 | "rodent.software/vole/metadata" |
|
c717d4eb
Keenan Nemetz
13d ago
|
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 | } |