detached 59d5a0b

inbox_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close