Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 2h ago 4b8c9e1d
history blame raw 6.9 KiB
1package index
2
3// A profile's numbers are a grouped read of the same event rows the feed shows.
4// These tests hold that: the count a profile displays is the number of rows the
5// feed would page through, the counts follow the repository set the caller
6// passes, and a release stays out because its actor is a Git tagger rather than
7// a credential.
8
9import (
10 "testing"
11 "time"
12
13 "rodent.software/vole/core/git"
14 "rodent.software/vole/metadata"
15)
16
17func TestActorActivityCountsTheFeedRows(t *testing.T) {
18 t.Parallel()
19 s, m, g := incrementalRepo(t)
20 other, og := newSecondRepo(t, s, "other")
21 now := time.Date(2026, 11, 1, 12, 0, 0, 0, time.UTC)
22
23 // alice opens two issues in demo and one in other; adam opens one of demo's.
24 for _, spec := range []struct {
25 g *git.Repo
26 title string
27 author string
28 at time.Time
29 }{
30 {g, "first", "alice", now},
31 {g, "second", "alice", now.Add(time.Minute)},
32 {og, "elsewhere", "alice", now.Add(2 * time.Minute)},
33 {g, "by adam", "adam", now.Add(3 * time.Minute)},
34 } {
35 iss, err := metadata.NewIssue(spec.title, "body", spec.author, nil, nil, spec.at)
36 if err != nil {
37 t.Fatal(err)
38 }
39 if err := metadata.CreateIssue(spec.g, iss, spec.author); err != nil {
40 t.Fatal(err)
41 }
42 }
43
44 // A comment from alice, a change from alice, and an approval by alice.
45 iss, err := metadata.NewIssue("commented", "body", "adam", nil, nil, now.Add(4*time.Minute))
46 if err != nil {
47 t.Fatal(err)
48 }
49 if err := metadata.CreateIssue(g, iss, "adam"); err != nil {
50 t.Fatal(err)
51 }
52 comment, err := metadata.NewIssueComment("a note", "alice", now.Add(5*time.Minute))
53 if err != nil {
54 t.Fatal(err)
55 }
56 if err := metadata.AddIssueComment(g, iss.ID, comment, "alice"); err != nil {
57 t.Fatal(err)
58 }
59 req, err := metadata.NewRequest("a change", "why", "adam", "main", requestHead, now.Add(6*time.Minute))
60 if err != nil {
61 t.Fatal(err)
62 }
63 if err := metadata.CreateRequest(g, req, "adam"); err != nil {
64 t.Fatal(err)
65 }
66 // An account cannot approve its own change, so a review is always of somebody
67 // else's work.
68 if _, err := metadata.ApproveRequest(g, req.ID, "alice", "main", requestHead, now.Add(7*time.Minute), "alice"); err != nil {
69 t.Fatal(err)
70 }
71
72 if err := s.Ensure(m); err != nil {
73 t.Fatal(err)
74 }
75 if err := s.Ensure(other); err != nil {
76 t.Fatal(err)
77 }
78
79 counts, err := s.ActorActivity([]string{m.ID, other.ID}, "alice")
80 if err != nil {
81 t.Fatal(err)
82 }
83 got := map[string]int{}
84 for _, c := range counts {
85 got[c.RepoID+"/"+c.Kind+"/"+c.Type+"/"+c.State] += c.N
86 }
87 for key, want := range map[string]int{
88 m.ID + "/issue/open/": 2,
89 other.ID + "/issue/open/": 1,
90 m.ID + "/issue/comment/": 1,
91 m.ID + "/change/approval/": 1,
92 } {
93 if got[key] != want {
94 t.Errorf("%s = %d, want %d (all counts: %v)", key, got[key], want, got)
95 }
96 }
97 // adam opened the change; alice only reviewed it, so the profile must not
98 // credit her with it.
99 if n := got[m.ID+"/change/open/"]; n != 0 {
100 t.Errorf("alice is credited with adam's change: %d", n)
101 }
102
103 // The caller decides visibility: a profile counted over one readable
104 // repository cannot include the other.
105 counts, err = s.ActorActivity([]string{m.ID}, "alice")
106 if err != nil {
107 t.Fatal(err)
108 }
109 for _, c := range counts {
110 if c.RepoID != m.ID {
111 t.Fatalf("a one-repository read returned counts for %s", c.RepoID)
112 }
113 }
114}
115
116func TestActorEventsFollowTheKindTypeAndVisibility(t *testing.T) {
117 t.Parallel()
118 s, m, g := incrementalRepo(t)
119 now := time.Date(2026, 11, 1, 12, 0, 0, 0, time.UTC)
120 iss, err := metadata.NewIssue("an issue", "body", "alice", nil, nil, now)
121 if err != nil {
122 t.Fatal(err)
123 }
124 if err := metadata.CreateIssue(g, iss, "alice"); err != nil {
125 t.Fatal(err)
126 }
127 req, err := metadata.NewRequest("a change", "why", "adam", "main", requestHead, now.Add(time.Minute))
128 if err != nil {
129 t.Fatal(err)
130 }
131 if err := metadata.CreateRequest(g, req, "adam"); err != nil {
132 t.Fatal(err)
133 }
134 if _, err := metadata.ApproveRequest(g, req.ID, "alice", "main", requestHead, now.Add(2*time.Minute), "alice"); err != nil {
135 t.Fatal(err)
136 }
137 // A tag whose tagger is alice: a Git identity, not a credential.
138 tip, err := g.RevParse(metadata.Ref)
139 if err != nil || tip == "" {
140 t.Fatalf("resolve metadata tip: %q, %v", tip, err)
141 }
142 if err := g.WriteAnnotatedTag("v1", tip, "notes\n", "alice", false); err != nil {
143 t.Fatal(err)
144 }
145 if err := s.Ensure(m); err != nil {
146 t.Fatal(err)
147 }
148 if err := s.RefreshTags(m); err != nil {
149 t.Fatal(err)
150 }
151
152 // The instance feed does show the release.
153 all, err := s.Events([]string{m.ID}, "", "", "", 100, 0)
154 if err != nil {
155 t.Fatal(err)
156 }
157 var releases int
158 for _, row := range all {
159 if row.Kind == "release" {
160 releases++
161 }
162 }
163 if releases == 0 {
164 t.Fatal("the feed is missing the release event")
165 }
166
167 // A profile does not: a tagger is a name somebody typed into a tag.
168 events, err := s.ActorEvents([]string{m.ID}, "", "", "alice", 100, 0)
169 if err != nil {
170 t.Fatal(err)
171 }
172 if len(events) != 2 {
173 t.Fatalf("profile feed = %d events, want 2: %+v", len(events), events)
174 }
175 for _, row := range events {
176 if row.Kind == "release" {
177 t.Fatalf("profile feed includes a release: %+v", row)
178 }
179 }
180
181 reviews, err := s.ActorEvents([]string{m.ID}, "", "approval", "alice", 100, 0)
182 if err != nil {
183 t.Fatal(err)
184 }
185 if len(reviews) != 1 || reviews[0].Type != "approval" {
186 t.Fatalf("reviews = %+v, want one approval", reviews)
187 }
188 if n, err := s.ActorEventCount([]string{m.ID}, "", "approval", "alice"); err != nil || n != 1 {
189 t.Fatalf("review count = %d, %v", n, err)
190 }
191
192 // A repository the caller does not pass contributes nothing.
193 events, err = s.ActorEvents([]string{"somewhere-else"}, "", "", "alice", 100, 0)
194 if err != nil {
195 t.Fatal(err)
196 }
197 if len(events) != 0 {
198 t.Fatalf("an unreadable repository returned %d events", len(events))
199 }
200}
201
202func TestActorSummariesAnswersForEveryone(t *testing.T) {
203 t.Parallel()
204 s, m, g := incrementalRepo(t)
205 now := time.Date(2026, 11, 1, 12, 0, 0, 0, time.UTC)
206 for _, spec := range []struct{ title, author string }{
207 {"one", "alice"}, {"two", "alice"}, {"three", "adam"},
208 } {
209 iss, err := metadata.NewIssue(spec.title, "body", spec.author, nil, nil, now)
210 if err != nil {
211 t.Fatal(err)
212 }
213 if err := metadata.CreateIssue(g, iss, spec.author); err != nil {
214 t.Fatal(err)
215 }
216 now = now.Add(time.Minute)
217 }
218 if err := s.Ensure(m); err != nil {
219 t.Fatal(err)
220 }
221
222 sums, err := s.ActorSummaries([]string{m.ID})
223 if err != nil {
224 t.Fatal(err)
225 }
226 if got := sums["alice"]; got.Repos != 1 || got.Issues != 2 || got.Changes != 0 || got.Reviews != 0 {
227 t.Fatalf("alice = %+v, want 1 repository and 2 issues", got)
228 }
229 if got := sums["adam"]; got.Issues != 1 {
230 t.Fatalf("adam = %+v, want 1 issue", got)
231 }
232 if len(sums) != 2 {
233 t.Fatalf("summaries = %+v, want only the two authors with visible work", sums)
234 }
235 // An empty repository set is not an error, it is an empty answer.
236 if sums, err = s.ActorSummaries(nil); err != nil || len(sums) != 0 {
237 t.Fatalf("empty repository set = %v, %v", sums, err)
238 }
239}
240