1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6 "context"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13
14 "bnema.dev/ermite/internal/domain"
15 "bnema.dev/ermite/internal/ports"
16 "bnema.dev/ermite/internal/ports/mocks"
17 "bnema.dev/ermite/internal/usecase"
18)
19
20func inboxMR(t *testing.T, repo string, n int64, up time.Time) domain.MergeRequest {
21 t.Helper()
22 ti, err := domain.ParseMRTitle("mr")
23 require.NoError(t, err)
24 return domain.MergeRequest{Repo: name(t, repo), Number: n, Title: ti, State: domain.MRStateOpen, UpdatedAt: up}
25}
26
27func inboxIssue(t *testing.T, repo string, n int64, up time.Time) domain.Issue {
28 t.Helper()
29 ti, err := domain.ParseIssueTitle("issue")
30 require.NoError(t, err)
31 return domain.Issue{Repo: name(t, repo), Number: n, Title: ti, State: domain.IssueStateOpen, UpdatedAt: up}
32}
33
34func newInboxUnit(t *testing.T) (*usecase.Inbox, *mocks.MergeRequestStore, *mocks.IssueStore, *mocks.RepoMetaStore) {
35 t.Helper()
36 mrs, is, meta := mocks.NewMergeRequestStore(t), mocks.NewIssueStore(t), mocks.NewRepoMetaStore(t)
37 return usecase.NewInbox(usecase.InboxConfig{Meta: meta, MergeRequests: mrs, Issues: is}), mrs, is, meta
38}
39
40func repoWithIssues(t *testing.T, n string, mode domain.IssuesMode) domain.Repo {
41 s := domain.DefaultRepoSettings()
42 s.Issues = mode
43 return domain.Repo{Name: name(t, n), Visibility: domain.VisibilityPrivate, Settings: s}
44}
45
46func TestInboxIsOwnerOnly(t *testing.T) {
47 t.Parallel()
48 in, _, _, _ := newInboxUnit(t) // the stores are never read
49 for _, p := range []domain.Principal{domain.Anonymous(), issuePrincipal(domain.ScopeIssueWrite)} {
50 _, err := in.Overview(context.Background(), p, domain.RepoName{})
51 assert.ErrorIs(t, err, domain.ErrNotFound)
52 }
53}
54
55func TestInboxOverviewSortsFiltersAndSkipsIssuesOff(t *testing.T) {
56 t.Parallel()
57 ctx := context.Background()
58 t0 := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
59 in, mrs, is, meta := newInboxUnit(t)
60 mrs.EXPECT().ListOpenAll(mock.Anything).Return([]domain.MergeRequest{
61 inboxMR(t, "alpha", 1, t0), inboxMR(t, "beta", 1, t0.Add(time.Hour)), inboxMR(t, "gone", 1, t0),
62 }, nil)
63 is.EXPECT().ListOpenAll(mock.Anything).Return([]domain.Issue{
64 inboxIssue(t, "alpha", 2, t0), inboxIssue(t, "beta", 2, t0.Add(2*time.Hour)), inboxIssue(t, "quiet", 2, t0),
65 }, nil)
66 meta.EXPECT().Get(mock.Anything, name(t, "alpha")).Return(repoWithIssues(t, "alpha", domain.IssuesOwner), nil)
67 meta.EXPECT().Get(mock.Anything, name(t, "beta")).Return(repoWithIssues(t, "beta", domain.IssuesPublic), nil)
68 meta.EXPECT().Get(mock.Anything, name(t, "quiet")).Return(repoWithIssues(t, "quiet", domain.IssuesOff), nil)
69 meta.EXPECT().Get(mock.Anything, name(t, "gone")).Return(domain.Repo{}, domain.ErrNotFound)
70
71 got, err := in.Overview(ctx, domain.Owner(), domain.RepoName{})
72 require.NoError(t, err)
73 assert.Equal(t, ports.InboxCounts{MergeRequests: 2, Issues: 2}, got.Counts)
74 require.Len(t, got.MergeRequests, 2)
75 assert.Equal(t, "beta", got.MergeRequests[0].Repo.String(), "most recently updated first")
76 require.Len(t, got.Issues, 2)
77 assert.Equal(t, "beta", got.Issues[0].Repo.String())
78 assert.Equal(t, []domain.RepoName{name(t, "alpha"), name(t, "beta")}, got.Repos)
79
80 got, err = in.Overview(ctx, domain.Root(), name(t, "alpha"))
81 require.NoError(t, err)
82 assert.Equal(t, ports.InboxCounts{MergeRequests: 2, Issues: 2}, got.Counts, "counts ignore the filter")
83 require.Len(t, got.MergeRequests, 1)
84 require.Len(t, got.Issues, 1)
85 assert.Equal(t, "alpha", got.Issues[0].Repo.String())
86 assert.Len(t, got.Repos, 2, "the filter list keeps every repository")
87}
88
89func TestInboxListsPendingIssuesWithoutTheirHash(t *testing.T) {
90 t.Parallel()
91 ctx := context.Background()
92 mrs, is, meta, pending := mocks.NewMergeRequestStore(t), mocks.NewIssueStore(t), mocks.NewRepoMetaStore(t), mocks.NewPendingIssueStore(t)
93 in := usecase.NewInbox(usecase.InboxConfig{Meta: meta, MergeRequests: mrs, Issues: is, Pending: pending})
94 mrs.EXPECT().ListOpenAll(mock.Anything).Return(nil, nil)
95 is.EXPECT().ListOpenAll(mock.Anything).Return(nil, nil)
96 meta.EXPECT().Get(mock.Anything, name(t, "on")).Return(repoWithIssues(t, "on", domain.IssuesPublic), nil)
97 meta.EXPECT().Get(mock.Anything, name(t, "off")).Return(repoWithIssues(t, "off", domain.IssuesOff), nil)
98 ti, err := domain.ParseIssueTitle("hi")
99 require.NoError(t, err)
100 pending.EXPECT().List(mock.Anything, domain.RepoName{}, mock.Anything).Return(ports.PendingIssuePage{Issues: []domain.PendingIssue{
101 {ID: "a", Repo: name(t, "on"), Title: ti, IPHash: []byte("secret")},
102 {ID: "b", Repo: name(t, "off"), Title: ti},
103 }}, nil)
104 ov, err := in.Overview(ctx, domain.Owner(), name(t, "other"))
105 require.NoError(t, err)
106 assert.Equal(t, 1, ov.Counts.Pending, "issues off hides pending ones too")
107 require.Len(t, ov.Pending, 1)
108 assert.Nil(t, ov.Pending[0].IPHash)
109 assert.Equal(t, []domain.RepoName{name(t, "on")}, ov.Repos)
110}
111
112func TestInboxRuns(t *testing.T) {
113 t.Parallel()
114 mrs, issues, meta, ci := mocks.NewMergeRequestStore(t), mocks.NewIssueStore(t), mocks.NewRepoMetaStore(t), mocks.NewCIStore(t)
115 mrs.EXPECT().ListOpenAll(mock.Anything).Return(nil, nil)
116 issues.EXPECT().ListOpenAll(mock.Anything).Return(nil, nil)
117 now := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
118 alpha, beta := name(t, "alpha"), name(t, "beta")
119 ci.EXPECT().ListOpenRuns(mock.Anything).Return([]domain.CIRun{
120 {Repo: alpha, ID: 1, Status: domain.CIQueued, CreatedAt: now},
121 {Repo: beta, ID: 2, Status: domain.CIInProgress, CreatedAt: now.Add(time.Hour)},
122 {Repo: alpha, ID: 3, Status: domain.CICompleted, CreatedAt: now.Add(2 * time.Hour)},
123 {Repo: name(t, "gone"), ID: 4, Status: domain.CIQueued, CreatedAt: now},
124 }, nil)
125 for _, n := range []string{"alpha", "beta"} {
126 meta.EXPECT().Get(mock.Anything, name(t, n)).Return(repoWithIssues(t, n, domain.IssuesOff), nil)
127 }
128 meta.EXPECT().Get(mock.Anything, name(t, "gone")).Return(domain.Repo{}, domain.ErrNotFound)
129 in := usecase.NewInbox(usecase.InboxConfig{Meta: meta, MergeRequests: mrs, Issues: issues, CI: ci})
130 all, err := in.Overview(t.Context(), domain.Owner(), domain.RepoName{})
131 require.NoError(t, err)
132 assert.True(t, all.RunsEnabled)
133 assert.Equal(t, 2, all.Counts.Running)
134 require.Len(t, all.Runs, 2)
135 assert.Equal(t, int64(2), all.Runs[0].ID)
136 assert.Equal(t, int64(1), all.Runs[1].ID)
137 assert.Equal(t, []domain.RepoName{alpha, beta}, all.Repos)
138 filtered, err := in.Overview(t.Context(), domain.Owner(), alpha)
139 require.NoError(t, err)
140 assert.Equal(t, 2, filtered.Counts.Running)
141 require.Len(t, filtered.Runs, 1)
142 assert.Equal(t, alpha, filtered.Runs[0].Repo)
143}