v0.31.0 d5e0505

submissions_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package clientlimit_test
4
5import (
6	"fmt"
7	"sync"
8	"sync/atomic"
9	"testing"
10	"time"
11
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/require"
14
15	"bnema.dev/ermite/internal/clientlimit"
16)
17
18func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {
19	clk := &fakeClock{t: time.Date(2026, 10, 10, 8, 0, 0, 0, time.UTC)}
20	return clientlimit.NewSubmissionLimiter(clk.now), clk
21}
22
23func TestSubmissionHourWindow(t *testing.T) {
24	t.Parallel()
25	l, clk := newSubs()
26	for i := range clientlimit.PerHour {
27		ok, _, _ := l.Reserve(bg, "1.2.3.4")
28		require.True(t, ok, "submission %d", i)
29		clk.t = clk.t.Add(time.Minute)
30	}
31	ok, wait, _ := l.Reserve(bg, "1.2.3.4")
32	assert.False(t, ok)
33	assert.Equal(t, time.Hour-3*time.Minute, wait, "until the first one leaves the hour")
34
35	other, _, _ := l.Reserve(bg, "5.6.7.8")
36	assert.True(t, other, "another client is not affected")
37
38	clk.t = clk.t.Add(wait)
39	ok, _, _ = l.Reserve(bg, "1.2.3.4")
40	assert.True(t, ok, "the window slides")
41}
42
43func TestSubmissionDayWindow(t *testing.T) {
44	t.Parallel()
45	l, clk := newSubs()
46	start := clk.t
47	// 3 per hour, spaced so the hourly limit never bites: one every 2 hours.
48	for range clientlimit.PerDay {
49		ok, _, _ := l.Reserve(bg, "1.2.3.4")
50		require.True(t, ok)
51		clk.t = clk.t.Add(2 * time.Hour)
52	}
53	// 20h after start: hourly window empty, day window full.
54	ok, wait, _ := l.Reserve(bg, "1.2.3.4")
55	assert.False(t, ok)
56	assert.Equal(t, start.Add(clientlimit.DayWindow).Sub(clk.t), wait)
57
58	clk.t = start.Add(clientlimit.DayWindow)
59	ok, _, _ = l.Reserve(bg, "1.2.3.4")
60	assert.True(t, ok)
61}
62
63func TestSubmissionIPv6GroupsBy64(t *testing.T) {
64	t.Parallel()
65	l, _ := newSubs()
66	for i := range clientlimit.PerHour {
67		l.Reserve(bg, fmt.Sprintf("2001:db8:1:2::%x", i+1)) // same /64
68	}
69	ok, _, _ := l.Reserve(bg, "2001:db8:1:2:ffff::1")
70	assert.False(t, ok, "the whole /64 shares the budget")
71	ok, _, _ = l.Reserve(bg, "2001:db8:1:3::1")
72	assert.True(t, ok, "another /64 does not")
73	ok, _, _ = l.Reserve(bg, "::ffff:1.2.3.4")
74	assert.True(t, ok)
75	perAddr, _ := l.Recent(bg, "1.2.3.4")
76	assert.Equal(t, 1, perAddr, "a mapped address counts as its IPv4")
77}
78
79func TestSubmissionRecent(t *testing.T) {
80	t.Parallel()
81	l, clk := newSubs()
82	l.Reserve(bg, "1.1.1.1")
83	l.Reserve(bg, "1.1.1.1")
84	l.Reserve(bg, "2.2.2.2")
85	perAddr, global := l.Recent(bg, "1.1.1.1")
86	assert.Equal(t, 2, perAddr)
87	assert.Equal(t, 3, global)
88
89	clk.t = clk.t.Add(time.Hour)
90	perAddr, global = l.Recent(bg, "1.1.1.1")
91	assert.Zero(t, perAddr, "Recent looks at the last hour")
92	assert.Zero(t, global)
93}
94
95func TestSubmissionEvictsOldest(t *testing.T) {
96	t.Parallel()
97	l, clk := newSubs()
98	for i := range clientlimit.MaxTracked + 10 {
99		clk.t = clk.t.Add(time.Millisecond)
100		l.Reserve(bg, fmt.Sprintf("10.%d.%d.%d", i>>16&255, i>>8&255, i&255))
101	}
102	assert.LessOrEqual(t, l.Tracked(), clientlimit.MaxTracked, "bounded")
103	ok, _, _ := l.Reserve(bg, "10.0.0.0")
104	assert.True(t, ok, "the oldest were evicted")
105}
106
107func TestSubmissionReserveIsAtomic(t *testing.T) {
108	t.Parallel()
109	l, _ := newSubs()
110	const n = 50
111	var granted atomic.Int32
112	var wg sync.WaitGroup
113	start := make(chan struct{})
114	for range n {
115		wg.Add(1)
116		go func() {
117			defer wg.Done()
118			<-start
119			if ok, _, _ := l.Reserve(bg, "1.2.3.4"); ok {
120				granted.Add(1)
121			}
122		}()
123	}
124	close(start)
125	wg.Wait()
126	assert.Equal(t, int32(clientlimit.PerHour), granted.Load())
127}
128
129func TestSubmissionReleaseRefunds(t *testing.T) {
130	t.Parallel()
131	l, _ := newSubs()
132	var releases []func()
133	for range clientlimit.PerHour {
134		ok, _, rel := l.Reserve(bg, "1.2.3.4")
135		require.True(t, ok)
136		releases = append(releases, rel)
137	}
138	ok, _, rel := l.Reserve(bg, "1.2.3.4")
139	require.False(t, ok)
140	rel() // no-op on a refusal
141	releases[0]()
142	releases[0]() // idempotent
143	perAddr, global := l.Recent(bg, "1.2.3.4")
144	assert.Equal(t, clientlimit.PerHour-1, perAddr)
145	assert.Equal(t, clientlimit.PerHour-1, global)
146	ok, _, _ = l.Reserve(bg, "1.2.3.4")
147	assert.True(t, ok, "the refunded slot is usable again")
148}

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 44

↑↓ moveenter openesc close