1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6 "context"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14
15 "bnema.dev/ermite/internal/adapters/eventbus"
16 "bnema.dev/ermite/internal/adapters/kvstore"
17 "bnema.dev/ermite/internal/domain"
18 "bnema.dev/ermite/internal/ports"
19 "bnema.dev/ermite/internal/ports/mocks"
20 "bnema.dev/ermite/internal/usecase"
21)
22
23// Issues over the real store: shared numbers with merge requests, the life of
24// an issue and its events.
25func TestIssuesOverTheStore(t *testing.T) {
26 ctx := context.Background()
27 store, err := kvstore.Open(ctx, filepath.Join(t.TempDir(), "kv"))
28 require.NoError(t, err)
29 t.Cleanup(func() { require.NoError(t, store.Close(context.Background())) })
30 bus := eventbus.NewWithBuffer(64)
31 events := bus.Subscribe(ctx)
32 n := name(t, "proj")
33 settings := domain.DefaultRepoSettings()
34 settings.Issues = domain.IssuesOwner
35 require.NoError(t, store.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: now, Settings: settings}))
36
37 issues := usecase.NewIssues(usecase.IssuesConfig{Meta: store, Store: store.Issues(), MergeRequests: store.MergeRequests(), Bus: bus, Clock: fixedClock{}})
38 agent := domain.TokenPrincipal(domain.Token{ID: "t", Label: "bot", Scopes: []domain.Scope{domain.ScopeIssueWrite}, BranchPrefixes: []string{"x/"}})
39
40 mrTitle, err := domain.ParseMRTitle("Change")
41 require.NoError(t, err)
42 mr, err := store.MergeRequests().Create(ctx, domain.MergeRequest{
43 Repo: n, Title: mrTitle, State: domain.MRStateOpen, Source: "feat", Target: "main", Author: "a", CreatedAt: now, UpdatedAt: now,
44 })
45 require.NoError(t, err)
46 assert.EqualValues(t, 1, mr.Number)
47
48 issue, err := issues.Create(ctx, agent, n, ports.CreateIssueParams{Title: "Crash", Body: "details"})
49 require.NoError(t, err)
50 assert.EqualValues(t, 2, issue.Number, "the number follows the merge request")
51 assert.Equal(t, "bot", issue.Author)
52 repo, err := store.Get(ctx, n)
53 require.NoError(t, err)
54 assert.True(t, repo.ActiveAt.Equal(now))
55 assert.True(t, repo.PushedAt.IsZero())
56
57 kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1)
58 require.NoError(t, err)
59 assert.Equal(t, domain.NumberMergeRequest, kind)
60 kind, err = issues.Kind(ctx, domain.Anonymous(), n, 2)
61 require.NoError(t, err)
62 assert.Equal(t, domain.NumberIssue, kind)
63 kind, err = issues.Kind(ctx, domain.Anonymous(), n, 3)
64 require.NoError(t, err)
65 assert.Equal(t, domain.NumberNone, kind)
66
67 _, err = issues.AddComment(ctx, agent, n, 2, "me too")
68 require.NoError(t, err)
69 _, err = issues.Close(ctx, agent, n, 2)
70 require.NoError(t, err)
71 _, err = issues.AddComment(ctx, agent, n, 2, "after close")
72 require.NoError(t, err)
73 _, err = issues.Close(ctx, agent, n, 2)
74 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)
75 _, err = issues.Reopen(ctx, agent, n, 2)
76 require.NoError(t, err)
77
78 d, err := issues.Get(ctx, domain.Anonymous(), n, 2)
79 require.NoError(t, err)
80 assert.Equal(t, domain.IssueStateOpen, d.Issue.State)
81 require.Len(t, d.Comments, 2)
82 assert.Equal(t, "me too", d.Comments[0].Text.String())
83 assert.EqualValues(t, 2, d.Issue.CommentSeq)
84
85 page, err := issues.List(ctx, domain.Anonymous(), n, ports.ListIssuesParams{State: domain.IssueStateClosed})
86 require.NoError(t, err)
87 assert.Empty(t, page.Issues)
88
89 var kinds []string
90 for len(events) > 0 {
91 kinds = append(kinds, (<-events).EventKind())
92 }
93 assert.Equal(t, []string{"issue.opened", "issue.commented", "issue.closed", "issue.commented", "issue.reopened"}, kinds)
94}
95
96// Moderated submissions are private work until approval makes them public activity.
97func TestModeratedIssueActivityOverStore(t *testing.T) {
98 ctx := t.Context()
99 store, err := kvstore.Open(ctx, t.TempDir())
100 require.NoError(t, err)
101 t.Cleanup(func() { require.NoError(t, store.Close(ctx)) })
102 n := name(t, "proj")
103 initial := now.Add(-time.Hour)
104 require.NoError(t, store.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: initial, ActiveAt: initial, Settings: withIssues(domain.IssuesPublic)}))
105 clock := mocks.NewClock(t)
106 at := now
107 clock.EXPECT().Now().RunAndReturn(func() time.Time { return at }).Maybe()
108 keys, err := usecase.NewIssueKeys(ctx, store.IssueKeys())
109 require.NoError(t, err)
110 limiter := mocks.NewSubmissionLimiter(t)
111 limiter.EXPECT().Recent(mock.Anything, mock.Anything).Return(0, 0).Maybe()
112 limiter.EXPECT().Reserve(mock.Anything, mock.Anything).Return(true, time.Duration(0), func() {}).Maybe()
113 bus := mocks.NewEventBus(t)
114 var kinds []string
115 bus.EXPECT().Publish(mock.Anything, mock.Anything).Run(func(_ context.Context, ev domain.Event) {
116 repo, err := store.Get(ctx, n)
117 require.NoError(t, err)
118 if ev.EventKind() == "issue.opened" {
119 require.Equal(t, at, repo.ActiveAt)
120 } else {
121 require.Equal(t, initial, repo.ActiveAt)
122 }
123 kinds = append(kinds, ev.EventKind())
124 }).Maybe()
125 submissions := usecase.NewIssueSubmissions(usecase.IssueSubmissionsConfig{Meta: store, Pending: store.PendingIssues(), Bans: store.IssueBans(), Limiter: limiter, Keys: keys, Bus: bus, Clock: clock, BaseDifficulty: testBits, MaxDifficulty: testBits})
126 moderation := usecase.NewIssueModeration(usecase.IssueModerationConfig{Meta: store, Pending: store.PendingIssues(), Bans: store.IssueBans(), Bus: bus, Clock: clock})
127 submit := func() domain.PendingIssueID {
128 ch, err := submissions.Challenge(ctx, n, "client")
129 require.NoError(t, err)
130 sub := ports.IssueSubmission{Token: ch.Token, FormToken: ch.FormToken, Solution: solve(t, ch), Title: "Bug", Body: "Details"}
131 at = at.Add(usecase.MinFillTime)
132 require.NoError(t, submissions.Submit(ctx, n, "client", sub))
133 repo, err := store.Get(ctx, n)
134 require.NoError(t, err)
135 require.Equal(t, initial, repo.ActiveAt)
136 page, err := store.PendingIssues().List(ctx, n, ports.ListPendingParams{PageSize: 10})
137 require.NoError(t, err)
138 require.Len(t, page.Issues, 1)
139 return page.Issues[0].ID
140 }
141 id := submit()
142 require.NoError(t, moderation.Reject(ctx, domain.Owner(), n, id, false))
143 repo, err := store.Get(ctx, n)
144 require.NoError(t, err)
145 require.Equal(t, initial, repo.ActiveAt)
146 id = submit()
147 _, err = moderation.Approve(ctx, domain.Owner(), n, id)
148 require.NoError(t, err)
149 repo, err = store.Get(ctx, n)
150 require.NoError(t, err)
151 require.Equal(t, at, repo.ActiveAt)
152 require.True(t, repo.PushedAt.IsZero())
153 require.Equal(t, []string{"issue.submitted", "issue.pending_removed", "issue.submitted", "issue.opened"}, kinds)
154}