detached 59d5a0b

mirror_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore_test
4
5import (
6	"bytes"
7	"context"
8	"strings"
9	"testing"
10	"unicode/utf8"
11
12	"github.com/stretchr/testify/require"
13
14	"bnema.dev/ermite/internal/domain"
15)
16
17func TestMirrorRoundTripsAndDelete(t *testing.T) {
18	ctx := context.Background()
19	s := openStore(t, t.TempDir())
20	repo := name(t, "mirror")
21	g, err := s.GetGlobal(ctx)
22	require.NoError(t, err)
23	require.Equal(t, domain.MirrorGlobalConfig{}, g)
24	_, err = s.GetRepoConfig(ctx, repo)
25	require.ErrorIs(t, err, domain.ErrNotFound)
26	_, err = s.GetStatus(ctx, repo)
27	require.ErrorIs(t, err, domain.ErrNotFound)
28	_, err = s.GetSnapshot(ctx, repo)
29	require.ErrorIs(t, err, domain.ErrNotFound)
30	_, _, err = s.GetMirrorKey(ctx)
31	require.ErrorIs(t, err, domain.ErrNotFound)
32	require.NoError(t, s.DeleteRepoConfig(ctx, repo))
33	require.ErrorIs(t, s.PutRepoConfig(ctx, domain.MirrorRepoConfig{Repo: repo}), domain.ErrNotFound)
34	require.ErrorIs(t, s.PutStatus(ctx, domain.MirrorStatus{Repo: repo}), domain.ErrNotFound)
35	require.ErrorIs(t, s.PutSnapshot(ctx, repo, domain.MirrorSnapshot{}), domain.ErrNotFound)
36	require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPrivate}))
37	g = domain.MirrorGlobalConfig{Enabled: true, URLTemplate: "git@example.org:{repo}", Cron: "*/5 * * * *", AllBranches: true, UpdatedAt: created}
38	require.NoError(t, s.PutGlobal(ctx, g))
39	gg, err := s.GetGlobal(ctx)
40	require.NoError(t, err)
41	require.Equal(t, g, gg)
42	all := false
43	c := domain.MirrorRepoConfig{Repo: repo, URL: "git@example.org:override", Cron: "0 * * * *", AllBranches: &all, Excluded: true, UpdatedAt: created}
44	require.NoError(t, s.PutRepoConfig(ctx, c))
45	cc, err := s.GetRepoConfig(ctx, repo)
46	require.NoError(t, err)
47	require.Equal(t, c, cc)
48	cs, err := s.ListRepoConfigs(ctx)
49	require.NoError(t, err)
50	require.Equal(t, []domain.MirrorRepoConfig{c}, cs)
51	c.AllBranches = nil
52	require.NoError(t, s.PutRepoConfig(ctx, c))
53	cc, err = s.GetRepoConfig(ctx, repo)
54	require.NoError(t, err)
55	require.Nil(t, cc.AllBranches)
56	st := domain.MirrorStatus{Repo: repo, State: domain.MirrorFailing, Class: domain.MirrorClassTransient, Reason: "retry", Detail: "network", ConsecutiveFailures: 3, LastAttemptAt: created, LastSuccessAt: created, NextRetryAt: created, PausedAt: created, UpdatedAt: created}
57	require.NoError(t, s.PutStatus(ctx, st))
58	ss, err := s.GetStatus(ctx, repo)
59	require.NoError(t, err)
60	require.Equal(t, st, ss)
61	statuses, err := s.ListStatus(ctx)
62	require.NoError(t, err)
63	require.Equal(t, []domain.MirrorStatus{st}, statuses)
64	id, err := domain.ParseObjectID("0123456789012345678901234567890123456789")
65	require.NoError(t, err)
66	snap := domain.MirrorSnapshot{URL: c.URL, AllBranches: true, At: created, Refs: []domain.MirrorRef{{Name: "refs/heads/main", ID: id}}}
67	require.NoError(t, s.PutSnapshot(ctx, repo, snap))
68	got, err := s.GetSnapshot(ctx, repo)
69	require.NoError(t, err)
70	require.Equal(t, snap, got)
71	sealed := []byte("opaque sealed seed")
72	require.NoError(t, s.PutMirrorKey(ctx, sealed, created, true))
73	require.ErrorIs(t, s.PutMirrorKey(ctx, []byte("other"), created, true), domain.ErrAlreadyExists)
74	blob, at, err := s.GetMirrorKey(ctx)
75	require.NoError(t, err)
76	require.Equal(t, sealed, blob)
77	require.Equal(t, created, at)
78	raw, ok := s.RawMirrorKey(ctx)
79	require.True(t, ok)
80	require.False(t, bytes.Equal(raw, sealed), "raw record is a protobuf envelope, not bare key material")
81	require.NoError(t, s.PutMirrorKey(ctx, []byte("rotated"), created, false))
82	blob, _, err = s.GetMirrorKey(ctx)
83	require.NoError(t, err)
84	require.Equal(t, []byte("rotated"), blob)
85	require.NoError(t, s.Delete(ctx, repo))
86	_, err = s.GetRepoConfig(ctx, repo)
87	require.ErrorIs(t, err, domain.ErrNotFound)
88	_, err = s.GetStatus(ctx, repo)
89	require.ErrorIs(t, err, domain.ErrNotFound)
90	_, err = s.GetSnapshot(ctx, repo)
91	require.ErrorIs(t, err, domain.ErrNotFound)
92	_, _, err = s.GetMirrorKey(ctx)
93	require.NoError(t, err)
94	gg, err = s.GetGlobal(ctx)
95	require.NoError(t, err)
96	require.Equal(t, g, gg)
97	require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPrivate}))
98	_, err = s.GetStatus(ctx, repo)
99	require.ErrorIs(t, err, domain.ErrNotFound)
100}
101
102func TestMirrorStatusInvalidUTF8IsRepairedNotPanicking(t *testing.T) {
103	ctx := context.Background()
104	s := openStore(t, t.TempDir())
105	repo := name(t, "mirror")
106	require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPrivate}))
107	detail := strings.Repeat("é", 2048) + "\xff tail"
108	require.NotPanics(t, func() {
109		require.NoError(t, s.PutStatus(ctx, domain.MirrorStatus{Repo: repo, State: domain.MirrorFailing, Detail: detail, Reason: "bad \xfe"}))
110	})
111	got, err := s.GetStatus(ctx, repo)
112	require.NoError(t, err)
113	require.True(t, utf8.ValidString(got.Detail))
114	require.True(t, utf8.ValidString(got.Reason))
115	require.Contains(t, got.Detail, "? tail")
116	// Other records refuse invalid text instead of panicking.
117	require.NotPanics(t, func() {
118		err := s.PutGlobal(ctx, domain.MirrorGlobalConfig{URLTemplate: "\xff"})
119		require.ErrorIs(t, err, domain.ErrInvalid)
120	})
121	require.NotPanics(t, func() {
122		err := s.PutRepoConfig(ctx, domain.MirrorRepoConfig{Repo: repo, URL: "\xff"})
123		require.ErrorIs(t, err, domain.ErrInvalid)
124	})
125}

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