| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/replication" |
| 12 | ) |
| 13 | |
| 14 | func TestReviewAnchorShape(t *testing.T) { |
| 15 | base := [][]string{{"a" , "30617:" + strings.Repeat("a" , 64) + ":test" }, {"E" , strings.Repeat("b" , 64), "" , strings.Repeat("a" , 64)}, {"K" , "1618" }, {"P" , strings.Repeat("a" , 64)}} |
| 16 | comment := func(kind string, extra ...[]string) event.Event { |
| 17 | tags := append([][]string(nil), base...) |
| 18 | tags[2] = []string{"K" , kind} |
| 19 | return event.Event{Kind: 1111, Tags: append(tags, extra...)} |
| 20 | } |
| 21 | anchor, present, err := reviewAnchorOf(comment("1618" , []string{"file" , "src/main.go" }, []string{"line" , "12" , "new" })) |
| 22 | if err != nil || !present || anchor != (reviewAnchor{File: "src/main.go" , Line: 12, Side: "new" }) { |
| 23 | t.Fatalf("valid anchor: %+v %v %v" , anchor, present, err) |
| 24 | } |
| 25 | if _, present, err := reviewAnchorOf(comment("1621" )); err != nil || present { |
| 26 | t.Fatalf("plain comment: %v %v" , present, err) |
| 27 | } |
| 28 | cases := map[string][][]string{ |
| 29 | "missing line" : {{"file" , "a" }}, |
| 30 | "missing file" : {{"line" , "1" , "new" }}, |
| 31 | "two files" : {{"file" , "a" }, {"file" , "b" }, {"line" , "1" , "new" }}, |
| 32 | "empty path" : {{"file" , " " }, {"line" , "1" , "new" }}, |
| 33 | "zero line" : {{"file" , "a" }, {"line" , "0" , "new" }}, |
| 34 | "padded line" : {{"file" , "a" }, {"line" , "01" , "new" }}, |
| 35 | "word line" : {{"file" , "a" }, {"line" , "one" , "new" }}, |
| 36 | "no side" : {{"file" , "a" }, {"line" , "1" }}, |
| 37 | "unknown side" : {{"file" , "a" }, {"line" , "1" , "left" }}, |
| 38 | "newline in path" : {{"file" , "a\nb" }, {"line" , "1" , "new" }}, |
| 39 | } |
| 40 | for name, extra := range cases { |
| 41 | if err := validateReviewAnchor(comment("1618" , extra...)); err == nil || !strings.HasPrefix(err.Error(), "blocked: " ) { |
| 42 | t.Fatalf("%s: %v" , name, err) |
| 43 | } |
| 44 | } |
| 45 | if err := validateReviewAnchor(comment("1621" , []string{"file" , "a" }, []string{"line" , "1" , "new" })); err == nil || !strings.Contains(err.Error(), "pull request or patch" ) { |
| 46 | t.Fatalf("anchor under an issue: %v" , err) |
| 47 | } |
| 48 | if err := validateReviewAnchor(comment("1617" , []string{"file" , "a" }, []string{"line" , "3" , "old" })); err != nil { |
| 49 | t.Fatalf("anchor under a patch: %v" , err) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestReviewCommentsAreValidatedAndBrowsedWithTheirAnchor(t *testing.T) { |
| 54 | ctx := context.Background() |
| 55 | _, tenant := testTenant(t) |
| 56 | p := tenant.Policy() |
| 57 | p.Features.Grasp = true |
| 58 | if err := tenant.applyPolicy(ctx, p); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | owner, _ := event.PublicKey(testOwnerSecret) |
| 62 | now := time.Now().Unix() |
| 63 | agentTestRepo(t, tenant, "review" ) |
| 64 | coordinate := "30617:" + owner + ":review" |
| 65 | pull := signedEvent(t, testOwnerSecret, event.KIND_GIT_PR, now, [][]string{{"a" , coordinate}, {"p" , owner}, {"subject" , "Change" }, {"c" , strings.Repeat("1" , 40)}, {"clone" , "https://example.test/review.git" }}, "Why" ) |
| 66 | if err := publishAs(t, tenant, pull); err != nil { |
| 67 | t.Fatalf("pull request rejected: %v" , err) |
| 68 | } |
| 69 | comment := func(createdAt int64, extra ...[]string) event.Event { |
| 70 | tags := append([][]string{{"a" , coordinate}, {"E" , pull.ID, "" , owner}, {"K" , "1618" }, {"P" , owner}, {"e" , pull.ID, "" , owner}, {"k" , "1618" }, {"p" , owner}}, extra...) |
| 71 | return signedEvent(t, testOwnerSecret, 1111, createdAt, tags, "Review note" ) |
| 72 | } |
| 73 | if err := publishAs(t, tenant, comment(now+1, []string{"file" , "README" }, []string{"line" , "2" , "left" })); err == nil || !strings.HasPrefix(err.Error(), "blocked: a review comment" ) { |
| 74 | t.Fatalf("malformed anchor (bad side): %v" , err) |
| 75 | } |
| 76 | if err := publishAs(t, tenant, comment(now+2, []string{"file" , "README" }, []string{"line" , "2" })); err == nil || !strings.HasPrefix(err.Error(), "blocked: a review comment" ) { |
| 77 | t.Fatalf("malformed anchor (no side): %v" , err) |
| 78 | } |
| 79 | if err := tenant.ingest(ctx, comment(now+3, []string{"file" , "" }, []string{"line" , "2" , "new" }), replication.OriginImport); err == nil || !strings.HasPrefix(err.Error(), "blocked: a review comment" ) { |
| 80 | t.Fatalf("malformed anchor over import: %v" , err) |
| 81 | } |
| 82 | anchored := comment(now+4, []string{"file" , "README" }, []string{"line" , "2" , "new" }) |
| 83 | if err := publishAs(t, tenant, anchored); err != nil { |
| 84 | t.Fatalf("anchored comment rejected: %v" , err) |
| 85 | } |
| 86 | plain := comment(now + 5) |
| 87 | if err := publishAs(t, tenant, plain); err != nil { |
| 88 | t.Fatalf("plain comment rejected: %v" , err) |
| 89 | } |
| 90 | raw, _ := json.Marshal(map[string]any{"owner" : owner, "repo" : "review" , "event" : pull.ID}) |
| 91 | result, err := tenant.Execute(ctx, "" , "browsepull" , []json.RawMessage{raw}) |
| 92 | if err != nil { |
| 93 | t.Fatalf("browsepull: %v" , err) |
| 94 | } |
| 95 | encoded, _ := json.Marshal(result) |
| 96 | var detail struct { |
| 97 | Replies []struct { |
| 98 | ID string `json:"id"` |
| 99 | File string `json:"file"` |
| 100 | Line int `json:"line"` |
| 101 | Side string `json:"side"` |
| 102 | } `json:"replies"` |
| 103 | } |
| 104 | if err := json.Unmarshal(encoded, &detail); err != nil || len(detail.Replies) != 2 { |
| 105 | t.Fatalf("replies = %s %v" , encoded, err) |
| 106 | } |
| 107 | if detail.Replies[0].ID != plain.ID || detail.Replies[0].File != "" || detail.Replies[0].Line != 0 || detail.Replies[0].Side != "" { |
| 108 | t.Fatalf("plain reply carried an anchor: %+v" , detail.Replies[0]) |
| 109 | } |
| 110 | if detail.Replies[1].ID != anchored.ID || detail.Replies[1].File != "README" || detail.Replies[1].Line != 2 || detail.Replies[1].Side != "new" { |
| 111 | t.Fatalf("anchored reply = %+v" , detail.Replies[1]) |
| 112 | } |
| 113 | } |