1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6 "context"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "io"
11 "runtime"
12 "strconv"
13 "strings"
14 "sync"
15 "sync/atomic"
16 "testing"
17 "time"
18
19 "github.com/stretchr/testify/assert"
20 "github.com/stretchr/testify/mock"
21 "github.com/stretchr/testify/require"
22
23 "bnema.dev/ermite/internal/domain"
24 "bnema.dev/ermite/internal/ports"
25 "bnema.dev/ermite/internal/ports/mocks"
26 "bnema.dev/ermite/internal/usecase"
27)
28
29type browseFixture struct {
30 meta *mocks.RepoMetaStore
31 reader *mocks.RepoReader
32 browse *usecase.Browse
33}
34
35func newBrowseFixture(t *testing.T) *browseFixture {
36 t.Helper()
37 f := &browseFixture{meta: mocks.NewRepoMetaStore(t), reader: mocks.NewRepoReader(t)}
38 f.browse = usecase.NewBrowse(f.meta, f.reader)
39 return f
40}
41
42func oid(c string) domain.ObjectID {
43 id, err := domain.ParseObjectID(strings.Repeat(c, 40))
44 if err != nil {
45 panic(err)
46 }
47 return id
48}
49
50// browseOps are the read use cases, each as a call that returns its error.
51// The reader expectation of each is what must (not) run.
52type browseOp struct {
53 name string
54 call func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error
55 // expect registers the reader call a permitted caller makes.
56 expect func(r *mocks.RepoReader)
57}
58
59func browseOps() []browseOp {
60 root := domain.TreePath{}
61 return []browseOp{
62 {"ListRefs", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
63 _, err := b.ListRefs(ctx, p, repo, ports.ListRefsParams{})
64 return err
65 }, func(r *mocks.RepoReader) { r.EXPECT().ListRefs(mock.Anything, mock.Anything).Return(nil, nil) }},
66 {"ResolveRevision", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
67 _, err := b.ResolveRevision(ctx, p, repo, domain.Revision{})
68 return err
69 }, func(r *mocks.RepoReader) {
70 r.EXPECT().ResolveRevision(mock.Anything, mock.Anything, mock.Anything).Return(domain.ResolvedRevision{}, nil)
71 }},
72 {"ResolveRefPath", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
73 _, err := b.ResolveRefPath(ctx, p, repo, "main/x")
74 return err
75 }, func(r *mocks.RepoReader) {
76 r.EXPECT().ResolveRefPath(mock.Anything, mock.Anything, "main/x").Return(domain.RefPath{}, nil)
77 }},
78 {"GetTree", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
79 _, err := b.GetTree(ctx, p, repo, domain.Revision{}, root, true)
80 return err
81 }, func(r *mocks.RepoReader) {
82 r.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, true).Return(domain.Tree{}, nil)
83 }},
84 {"ListPaths", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
85 _, err := b.ListPaths(ctx, p, repo, domain.Revision{})
86 return err
87 }, func(r *mocks.RepoReader) {
88 r.EXPECT().ListPaths(mock.Anything, mock.Anything, mock.Anything, domain.MaxPathEntries, domain.MaxPathBytes).Return(domain.PathList{}, nil)
89 }},
90 {"OpenBlob", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
91 _, rc, err := b.OpenBlob(ctx, p, repo, domain.Revision{}, root)
92 if rc != nil {
93 _ = rc.Close()
94 }
95 return err
96 }, func(r *mocks.RepoReader) {
97 r.EXPECT().OpenBlob(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
98 Return(domain.BlobInfo{}, io.NopCloser(strings.NewReader("")), nil)
99 }},
100 {"ListCommits", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
101 _, err := b.ListCommits(ctx, p, repo, ports.ListCommitsParams{})
102 return err
103 }, func(r *mocks.RepoReader) {
104 r.EXPECT().ListCommits(mock.Anything, mock.Anything, mock.Anything).Return(ports.CommitPage{}, nil)
105 }},
106 {"GetCommit", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
107 _, err := b.GetCommit(ctx, p, repo, domain.Revision{})
108 return err
109 }, func(r *mocks.RepoReader) {
110 r.EXPECT().GetCommit(mock.Anything, mock.Anything, mock.Anything, domain.DefaultDiffLimits()).Return(domain.CommitDetail{}, nil)
111 }},
112 {"RepoStats", func(b *usecase.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
113 _, err := b.RepoStats(ctx, p, repo, oid("a"), time.Now())
114 return err
115 }, func(r *mocks.RepoReader) {
116 r.EXPECT().CountCommits(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(1, nil)
117 r.EXPECT().LatestTag(mock.Anything, mock.Anything).Return(domain.TagInfo{}, false, nil)
118 }},
119 }
120}
121
122// RepoStats reads Git once per state of the repository (head and last push)
123// and per 10 minutes; concurrent callers share the read, and a failure is
124// not kept.
125func TestRepoStatsCache(t *testing.T) {
126 t.Parallel()
127 ctx := context.Background()
128 f := newBrowseFixture(t)
129 repo := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic, CreatedAt: time.Unix(100, 0), PushedAt: time.Unix(200, 0)}
130 var mu sync.Mutex
131 f.meta.EXPECT().Get(mock.Anything, repo.Name).RunAndReturn(func(context.Context, domain.RepoName) (domain.Repo, error) {
132 mu.Lock()
133 defer mu.Unlock()
134 return repo, nil
135 })
136 now := time.Date(2026, 10, 18, 0, 0, 0, 0, time.UTC)
137 var reads atomic.Int32
138 gate := make(chan struct{})
139 f.reader.EXPECT().CountCommits(mock.Anything, repo.Name, mock.Anything, time.Time{}).RunAndReturn(
140 func(context.Context, domain.RepoName, domain.ObjectID, time.Time) (int, error) {
141 reads.Add(1)
142 <-gate
143 return 900, nil
144 })
145 f.reader.EXPECT().CountCommits(mock.Anything, repo.Name, mock.Anything, mock.MatchedBy(func(s time.Time) bool { return !s.IsZero() })).Return(12, nil)
146 tag := domain.TagInfo{Name: "v1", At: now, CommitID: oid("c")}
147 f.reader.EXPECT().LatestTag(mock.Anything, repo.Name).Return(tag, true, nil)
148
149 stats := func(head string, at time.Time) ports.RepoStats {
150 st, err := f.browse.RepoStats(ctx, domain.Anonymous(), repo.Name, oid(head), at)
151 require.NoError(t, err)
152 return st
153 }
154
155 // Eight concurrent requests: one read.
156 var wg sync.WaitGroup
157 for range 8 {
158 wg.Go(func() {
159 st := stats("a", now)
160 assert.Equal(t, 900, st.Commits)
161 assert.Equal(t, 12, st.Recent)
162 assert.Equal(t, tag, *st.LatestTag)
163 })
164 }
165 for reads.Load() == 0 {
166 runtime.Gosched()
167 }
168 close(gate)
169 wg.Wait()
170 assert.EqualValues(t, 1, reads.Load(), "concurrent callers share the read")
171
172 stats("a", now.Add(9*time.Minute))
173 assert.EqualValues(t, 1, reads.Load(), "served from the cache")
174 stats("a", now.Add(10*time.Minute))
175 assert.EqualValues(t, 2, reads.Load(), "expired")
176 stats("b", now.Add(10*time.Minute))
177 assert.EqualValues(t, 3, reads.Load(), "another head")
178 mu.Lock()
179 repo.PushedAt = time.Unix(300, 0) // a push, a tag-only one included
180 mu.Unlock()
181 stats("b", now.Add(10*time.Minute))
182 assert.EqualValues(t, 4, reads.Load(), "a push forgets the figures")
183}
184
185func TestRepoStatsDoesNotKeepAFailure(t *testing.T) {
186 t.Parallel()
187 ctx := context.Background()
188 f := newBrowseFixture(t)
189 repo := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic}
190 f.meta.EXPECT().Get(ctx, repo.Name).Return(repo, nil)
191 now := time.Now()
192 f.reader.EXPECT().CountCommits(mock.Anything, repo.Name, oid("a"), time.Time{}).Return(0, errors.New("boom")).Once()
193 _, err := f.browse.RepoStats(ctx, domain.Anonymous(), repo.Name, oid("a"), now)
194 require.Error(t, err)
195 f.reader.EXPECT().CountCommits(mock.Anything, repo.Name, oid("a"), mock.Anything).Return(3, nil).Twice()
196 f.reader.EXPECT().LatestTag(mock.Anything, repo.Name).Return(domain.TagInfo{}, false, nil).Once()
197 st, err := f.browse.RepoStats(ctx, domain.Anonymous(), repo.Name, oid("a"), now)
198 require.NoError(t, err)
199 assert.Equal(t, 3, st.Commits)
200 assert.Nil(t, st.LatestTag)
201}
202
203// TestBrowseAccess is the access rule over every read use case and every
204// principal: anonymous sees public repositories only, a private one answers
205// not found; stored tokens need repo:read on the repository; and the Git
206// reader is never reached when access is refused.
207func TestBrowseAccess(t *testing.T) {
208 t.Parallel()
209 ctx := context.Background()
210 pub := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic}
211 priv := domain.Repo{Name: name(t, "priv"), Visibility: domain.VisibilityPrivate}
212
213 principals := []struct {
214 name string
215 p domain.Principal
216 // who may read which repository
217 pub, priv bool
218 }{
219 {"anonymous", domain.Anonymous(), true, false},
220 {"root", domain.Root(), true, true},
221 {"token repo:read", tokenPrincipal(domain.ScopeRepoRead), true, true},
222 {"token repo:read on priv", tokenPrincipal(domain.ScopeRepoRead, "priv"), true, true},
223 {"token repo:read on another repo", tokenPrincipal(domain.ScopeRepoRead, "other"), true, false},
224 {"token repo:write only", tokenPrincipal(domain.ScopeRepoWrite), true, false},
225 {"token repo:admin only", tokenPrincipal(domain.ScopeRepoAdmin, "priv"), true, false},
226 {"token token:admin only", tokenPrincipal(domain.ScopeTokenAdmin), true, false},
227 }
228 for _, op := range browseOps() {
229 for _, pr := range principals {
230 for _, repo := range []domain.Repo{pub, priv} {
231 t.Run(op.name+"/"+pr.name+"/"+repo.Name.String(), func(t *testing.T) {
232 t.Parallel()
233 f := newBrowseFixture(t)
234 f.meta.EXPECT().Get(ctx, repo.Name).Return(repo, nil)
235 allowed := pr.pub
236 if repo.Visibility == domain.VisibilityPrivate {
237 allowed = pr.priv
238 }
239 if allowed {
240 op.expect(f.reader)
241 }
242 err := op.call(f.browse, ctx, pr.p, repo.Name)
243 if allowed {
244 assert.NoError(t, err)
245 } else {
246 assert.ErrorIs(t, err, domain.ErrNotFound)
247 assert.NotErrorIs(t, err, domain.ErrForbidden)
248 }
249 })
250 }
251 }
252 t.Run(op.name+"/missing repository", func(t *testing.T) {
253 t.Parallel()
254 f := newBrowseFixture(t)
255 f.meta.EXPECT().Get(ctx, name(t, "nope")).Return(domain.Repo{}, domain.ErrNotFound)
256 err := op.call(f.browse, ctx, domain.Root(), name(t, "nope"))
257 assert.ErrorIs(t, err, domain.ErrNotFound)
258 })
259 t.Run(op.name+"/zero name", func(t *testing.T) {
260 t.Parallel()
261 f := newBrowseFixture(t)
262 err := op.call(f.browse, ctx, domain.Root(), domain.RepoName{})
263 assert.ErrorIs(t, err, domain.ErrInvalidName)
264 })
265 }
266}
267
268func TestBrowseHidesPrivateOnReaderErrors(t *testing.T) {
269 t.Parallel()
270 // A reader error is passed through for a caller who may read.
271 ctx := context.Background()
272 f := newBrowseFixture(t)
273 pub := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic}
274 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil)
275 f.reader.EXPECT().GetTree(ctx, pub.Name, domain.Revision{}, domain.TreePath{}, false).Return(domain.Tree{}, domain.ErrNotDirectory)
276 _, err := f.browse.GetTree(ctx, domain.Anonymous(), pub.Name, domain.Revision{}, domain.TreePath{}, false)
277 assert.ErrorIs(t, err, domain.ErrNotDirectory)
278}
279
280func ref(kind domain.RefKind, name string) domain.Ref {
281 return domain.Ref{Name: name, Kind: kind, ShortName: name}
282}
283
284func TestListRefsPagingAndKinds(t *testing.T) {
285 t.Parallel()
286 ctx := context.Background()
287 pub := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic}
288 all := []domain.Ref{
289 ref(domain.RefKindHead, "HEAD"),
290 ref(domain.RefKindBranch, "refs/heads/a"),
291 ref(domain.RefKindBranch, "refs/heads/b"),
292 ref(domain.RefKindBranch, "refs/heads/c"),
293 ref(domain.RefKindTag, "refs/tags/v1"),
294 ref(domain.RefKindTag, "refs/tags/v2"),
295 }
296 names := func(refs []domain.Ref) []string {
297 var out []string
298 for _, r := range refs {
299 out = append(out, r.Name)
300 }
301 return out
302 }
303 setup := func(t *testing.T, refs []domain.Ref) *usecase.Browse {
304 f := newBrowseFixture(t)
305 f.meta.EXPECT().Get(mock.Anything, pub.Name).Return(pub, nil)
306 f.reader.EXPECT().ListRefs(mock.Anything, pub.Name).Return(refs, nil)
307 return f.browse
308 }
309
310 t.Run("pages follow each other without gap or repeat", func(t *testing.T) {
311 b := setup(t, all)
312 var got []string
313 tok := ""
314 for pages := 0; ; pages++ {
315 require.Less(t, pages, 10)
316 page, err := b.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageSize: 2, PageToken: tok})
317 require.NoError(t, err)
318 got = append(got, names(page.Refs)...)
319 if tok = page.NextPageToken; tok == "" {
320 break
321 }
322 }
323 assert.Equal(t, names(all), got)
324 })
325
326 t.Run("a ref that appears between pages does not shift the others", func(t *testing.T) {
327 b := setup(t, all[:4])
328 page, err := b.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageSize: 2})
329 require.NoError(t, err)
330 require.Equal(t, []string{"HEAD", "refs/heads/a"}, names(page.Refs))
331
332 b2 := setup(t, append([]domain.Ref{all[0], ref(domain.RefKindBranch, "refs/heads/0new")}, all[1:4]...))
333 next, err := b2.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageSize: 2, PageToken: page.NextPageToken})
334 require.NoError(t, err)
335 assert.Equal(t, []string{"refs/heads/b", "refs/heads/c"}, names(next.Refs))
336 })
337
338 t.Run("kinds filter", func(t *testing.T) {
339 b := setup(t, all)
340 page, err := b.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{Kinds: []domain.RefKind{domain.RefKindTag}})
341 require.NoError(t, err)
342 assert.Equal(t, []string{"refs/tags/v1", "refs/tags/v2"}, names(page.Refs))
343 assert.Empty(t, page.NextPageToken)
344 })
345
346 t.Run("default and maximum page size", func(t *testing.T) {
347 var many []domain.Ref
348 many = append(many, ref(domain.RefKindHead, "HEAD"))
349 for i := range usecase.MaxRefPageSize + 100 {
350 many = append(many, ref(domain.RefKindTag, fmt.Sprintf("refs/tags/t%04d", i)))
351 }
352 b := setup(t, many)
353 page, err := b.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{})
354 require.NoError(t, err)
355 assert.Len(t, page.Refs, usecase.DefaultPageSize)
356 assert.NotEmpty(t, page.NextPageToken)
357
358 b = setup(t, many)
359 page, err = b.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageSize: 100000})
360 require.NoError(t, err)
361 assert.Len(t, page.Refs, usecase.MaxRefPageSize, "a page holds up to MaxRefPageSize refs")
362 assert.NotEmpty(t, page.NextPageToken)
363 })
364
365 t.Run("bad input never reaches the reader", func(t *testing.T) {
366 t.Parallel()
367 f := newBrowseFixture(t)
368 for _, params := range []ports.ListRefsParams{
369 {PageSize: -1},
370 {Kinds: []domain.RefKind{"nope"}},
371 {PageToken: "!!!"},
372 {PageToken: "Y2E6eA"}, // a token of another kind of listing
373 } {
374 _, err := f.browse.ListRefs(ctx, domain.Anonymous(), pub.Name, params)
375 assert.Error(t, err, "%+v", params)
376 }
377 _, err := f.browse.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageSize: -1})
378 assert.ErrorIs(t, err, domain.ErrInvalidPageSize)
379 _, err = f.browse.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{Kinds: []domain.RefKind{"x"}})
380 assert.ErrorIs(t, err, domain.ErrInvalidRefKind)
381 _, err = f.browse.ListRefs(ctx, domain.Anonymous(), pub.Name, ports.ListRefsParams{PageToken: "!!!"})
382 assert.ErrorIs(t, err, domain.ErrInvalidPageToken)
383 })
384}
385
386func TestListCommitsPaging(t *testing.T) {
387 t.Parallel()
388 ctx := context.Background()
389 pub := domain.Repo{Name: name(t, "pub"), Visibility: domain.VisibilityPublic}
390 p := mustPath(t, "d/f")
391
392 t.Run("first page asks for one more than the page and returns a token", func(t *testing.T) {
393 t.Parallel()
394 f := newBrowseFixture(t)
395 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil)
396 f.reader.EXPECT().ListCommits(ctx, pub.Name, ports.CommitQuery{Path: p, Limit: 10}).
397 Return(ports.CommitPage{Head: oid("a"), Commits: []domain.Commit{{ID: oid("b")}}, HasMore: true}, nil)
398 page, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{Path: p, PageSize: 10})
399 require.NoError(t, err)
400 require.Len(t, page.Commits, 1)
401 require.NotEmpty(t, page.NextPageToken)
402
403 // The next page starts from the pinned head, after the commits read.
404 f.reader.EXPECT().ListCommits(ctx, pub.Name, ports.CommitQuery{
405 Revision: domain.RevisionOf(oid("a")), Path: p, Skip: 1, Limit: 10,
406 }).Return(ports.CommitPage{Head: oid("a"), Commits: []domain.Commit{{ID: oid("c")}}}, nil)
407 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil)
408 // The revision of the request is ignored once there is a token.
409 rev, _ := domain.ParseRevision("moved")
410 next, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{
411 Revision: rev, Path: p, PageSize: 10, PageToken: page.NextPageToken,
412 })
413 require.NoError(t, err)
414 assert.Equal(t, oid("c"), next.Commits[0].ID)
415 assert.Empty(t, next.NextPageToken)
416 })
417
418 t.Run("a token is bound to its path and must be well formed", func(t *testing.T) {
419 t.Parallel()
420 f := newBrowseFixture(t)
421 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil)
422 f.reader.EXPECT().ListCommits(ctx, pub.Name, mock.Anything).
423 Return(ports.CommitPage{Head: oid("a"), Commits: []domain.Commit{{ID: oid("b")}}, HasMore: true}, nil)
424 page, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{Path: p})
425 require.NoError(t, err)
426
427 for _, params := range []ports.ListCommitsParams{
428 {PageToken: page.NextPageToken}, // no path
429 {Path: mustPath(t, "other"), PageToken: page.NextPageToken}, // another path
430 {Path: p, PageToken: "garbage"},
431 {Path: p, PageToken: "cjE6eDp5OnQ"}, // c1:x:y:t is not a token
432 {Path: p, PageToken: strings.Repeat("A", 2000)},
433 } {
434 _, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, params)
435 assert.ErrorIs(t, err, domain.ErrInvalidPageToken, "%+v", params)
436 }
437 })
438
439 t.Run("a token cannot skip deeper than the cap", func(t *testing.T) {
440 t.Parallel()
441 f := newBrowseFixture(t)
442 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil).Maybe()
443 f.reader.EXPECT().ListCommits(ctx, pub.Name, mock.Anything).
444 Return(ports.CommitPage{Head: oid("a"), Commits: []domain.Commit{{ID: oid("b")}}, HasMore: true}, nil).Once()
445 page, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{Path: p})
446 require.NoError(t, err)
447 raw, err := base64.RawURLEncoding.DecodeString(page.NextPageToken)
448 require.NoError(t, err)
449 parts := strings.Split(string(raw), ":") // c1:<head>:<skip>:<path digest>
450 require.Len(t, parts, 4)
451 withSkip := func(n int) string {
452 parts[2] = strconv.Itoa(n)
453 return base64.RawURLEncoding.EncodeToString([]byte(strings.Join(parts, ":")))
454 }
455
456 f.reader.EXPECT().ListCommits(ctx, pub.Name, mock.MatchedBy(func(q ports.CommitQuery) bool { return q.Skip == usecase.MaxCommitSkip })).
457 Return(ports.CommitPage{Head: oid("a")}, nil).Once()
458 _, err = f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{Path: p, PageToken: withSkip(usecase.MaxCommitSkip)})
459 require.NoError(t, err, "the cap itself is allowed")
460 _, err = f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{Path: p, PageToken: withSkip(usecase.MaxCommitSkip + 1)})
461 assert.ErrorIs(t, err, domain.ErrInvalidPageToken)
462 })
463
464 t.Run("page size", func(t *testing.T) {
465 t.Parallel()
466 f := newBrowseFixture(t)
467 f.meta.EXPECT().Get(ctx, pub.Name).Return(pub, nil).Times(2)
468 f.reader.EXPECT().ListCommits(ctx, pub.Name, ports.CommitQuery{Limit: usecase.DefaultPageSize}).Return(ports.CommitPage{}, nil)
469 f.reader.EXPECT().ListCommits(ctx, pub.Name, ports.CommitQuery{Limit: 100}).Return(ports.CommitPage{}, nil)
470 _, err := f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{})
471 require.NoError(t, err)
472 _, err = f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{PageSize: 5000})
473 require.NoError(t, err)
474 _, err = f.browse.ListCommits(ctx, domain.Anonymous(), pub.Name, ports.ListCommitsParams{PageSize: -3})
475 assert.ErrorIs(t, err, domain.ErrInvalidPageSize)
476 })
477
478 t.Run("the token of a private repository does not open it", func(t *testing.T) {
479 priv := domain.Repo{Name: name(t, "priv"), Visibility: domain.VisibilityPrivate}
480 f := newBrowseFixture(t)
481 f.meta.EXPECT().Get(ctx, priv.Name).Return(priv, nil)
482 f.reader.EXPECT().ListCommits(ctx, priv.Name, mock.Anything).
483 Return(ports.CommitPage{Head: oid("a"), Commits: []domain.Commit{{ID: oid("b")}}, HasMore: true}, nil)
484 page, err := f.browse.ListCommits(ctx, domain.Root(), priv.Name, ports.ListCommitsParams{})
485 require.NoError(t, err)
486 f.meta.EXPECT().Get(ctx, priv.Name).Return(priv, nil)
487 _, err = f.browse.ListCommits(ctx, domain.Anonymous(), priv.Name, ports.ListCommitsParams{PageToken: page.NextPageToken})
488 assert.ErrorIs(t, err, domain.ErrNotFound)
489 })
490}
491
492func mustPath(t *testing.T, s string) domain.TreePath {
493 t.Helper()
494 p, err := domain.ParseTreePath(s)
495 require.NoError(t, err)
496 return p
497}