v0.5.1 31f302d

worker_linux_test.go RawHistoryFolder

1//go:build linux
2
3package auth
4
5import (
6	"bytes"
7	"context"
8	"errors"
9	"io"
10	"os"
11	"os/exec"
12	"sync"
13	"testing"
14	"time"
15
16	"github.com/bnema/neferafk/internal/ports"
17	"golang.org/x/sys/unix"
18)
19
20const hardenChildEnv = "NEFERAFK_TEST_HARDEN_CHILD"
21
22// TestHardenProcessDisablesDumpsAndCoreLimit runs in a subprocess because the
23// helper changes process-wide state.
24func TestHardenProcessDisablesDumpsAndCoreLimit(t *testing.T) {
25	if os.Getenv(hardenChildEnv) == "1" {
26		if dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0); err != nil || dumpable != 1 {
27			t.Fatalf("precondition: dumpable=%d err=%v", dumpable, err)
28		}
29		if err := HardenProcess(); err != nil {
30			t.Fatal(err)
31		}
32		if dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0); err != nil || dumpable != 0 {
33			t.Fatalf("dumpable=%d err=%v", dumpable, err)
34		}
35		var lim unix.Rlimit
36		if err := unix.Getrlimit(unix.RLIMIT_CORE, &lim); err != nil || lim.Cur != 0 || lim.Max != 0 {
37			t.Fatalf("RLIMIT_CORE %+v err=%v", lim, err)
38		}
39		return
40	}
41	cmd := exec.Command(os.Args[0], "-test.run=^TestHardenProcessDisablesDumpsAndCoreLimit$")
42	cmd.Env = append(os.Environ(), hardenChildEnv+"=1")
43	if out, err := cmd.CombinedOutput(); err != nil {
44		t.Fatalf("child: %v\n%s", err, out)
45	}
46}
47
48func TestDenyDelaySchedule(t *testing.T) {
49	want := []time.Duration{0, time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second, 30 * time.Second, 30 * time.Second, 30 * time.Second}
50	for n, w := range want {
51		if got := denyDelay(n); got != w {
52			t.Fatalf("denyDelay(%d)=%v want %v", n, got, w)
53		}
54	}
55}
56
57func readyClock() <-chan time.Time {
58	c := make(chan time.Time, 1)
59	c <- time.Time{}
60	return c
61}
62
63func attemptFrames(t *testing.T, in *bytes.Buffer, pins ...string) {
64	t.Helper()
65	for i, p := range pins {
66		if err := WriteFrame(in, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: uint64(i + 1), Payload: []byte(p)}); err != nil {
67			t.Fatal(err)
68		}
69	}
70}
71
72func TestWrongPINDelaysGrowThenCapAndSuccessIsNotDelayed(t *testing.T) {
73	var in, out bytes.Buffer
74	attemptFrames(t, &in, "000001", "000002", "000003", "000004", "000005", "000006", "123456")
75	clock := newMockdelayClock(t)
76	for _, d := range []time.Duration{1, 2, 4, 8, 30, 30} {
77		clock.EXPECT().after(d * time.Second).Return(readyClock()).Once()
78	}
79	// The right PIN is answered without any delay (no further clock call).
80	if err := runWith(context.Background(), clock, &in, &out, 8, []byte("123456"), nil, "", false); err != nil {
81		t.Fatal(err)
82	}
83}
84
85func TestWrongPINDelayScheduleRestartsForAFreshWorker(t *testing.T) {
86	// Success ends the worker, so the reset is observable as: the first wrong
87	// answer of every fresh worker waits 1s again (no shared counter).
88	for range 2 {
89		var in, out bytes.Buffer
90		attemptFrames(t, &in, "000001", "123456")
91		clock := newMockdelayClock(t)
92		clock.EXPECT().after(time.Second).Return(readyClock()).Once()
93		if err := runWith(context.Background(), clock, &in, &out, 8, []byte("123456"), nil, "", false); err != nil {
94			t.Fatal(err)
95		}
96	}
97}
98
99func TestDelayedDenialIsAnsweredOnlyAfterTheClockFires(t *testing.T) {
100	inR, inW := io.Pipe()
101	var out lockedBuffer
102	fire := make(chan time.Time)
103	clock := newMockdelayClock(t)
104	clock.EXPECT().after(time.Second).Return((<-chan time.Time)(fire)).Once()
105	done := make(chan error, 1)
106	go func() { done <- runWith(context.Background(), clock, inR, &out, 8, []byte("123456"), nil, "", false) }()
107	if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 1, Payload: []byte("000000")}); err != nil {
108		t.Fatal(err)
109	}
110	time.Sleep(50 * time.Millisecond)
111	if n := out.frames(); n != 1 { // ready only: no denial yet
112		t.Fatalf("%d frames before the delay elapsed", n)
113	}
114	fire <- time.Time{}
115	if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 2, Payload: []byte("123456")}); err != nil {
116		t.Fatal(err)
117	}
118	if err := <-done; err != nil {
119		t.Fatal(err)
120	}
121	if n := out.frames(); n != 3 {
122		t.Fatalf("%d frames", n)
123	}
124}
125
126func TestDelayEndsOnEOFAndCancellationWithoutAnswer(t *testing.T) {
127	for _, name := range []string{"eof", "cancel"} {
128		inR, inW := io.Pipe()
129		var out lockedBuffer
130		ctx, cancel := context.WithCancel(context.Background())
131		clock := newMockdelayClock(t)
132		clock.EXPECT().after(time.Second).Return((<-chan time.Time)(make(chan time.Time))).Once() // never fires
133		done := make(chan error, 1)
134		go func() { done <- runWith(ctx, clock, inR, &out, 8, []byte("123456"), nil, "", false) }()
135		if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 1, Payload: []byte("000000")}); err != nil {
136			t.Fatal(err)
137		}
138		want := io.EOF
139		if name == "eof" {
140			time.Sleep(20 * time.Millisecond)
141			inW.Close()
142		} else {
143			want = context.Canceled
144			time.Sleep(20 * time.Millisecond)
145			cancel()
146		}
147		select {
148		case err := <-done:
149			if !errors.Is(err, want) {
150				t.Fatalf("%s: %v", name, err)
151			}
152		case <-time.After(5 * time.Second):
153			t.Fatalf("%s: delay did not end", name)
154		}
155		if n := out.frames(); n != 1 {
156			t.Fatalf("%s: answered denial during the delay (%d frames)", name, n)
157		}
158		cancel()
159	}
160}
161
162type lockedBuffer struct {
163	mu sync.Mutex
164	b  bytes.Buffer
165}
166
167func (l *lockedBuffer) Write(p []byte) (int, error) {
168	l.mu.Lock()
169	defer l.mu.Unlock()
170	return l.b.Write(p)
171}
172
173func (l *lockedBuffer) frames() int {
174	l.mu.Lock()
175	cp := bytes.NewBuffer(bytes.Clone(l.b.Bytes()))
176	l.mu.Unlock()
177	n := 0
178	for {
179		if _, err := ReadFrame(cp); err != nil {
180			return n
181		}
182		n++
183	}
184}

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 9

↑↓ moveenter openesc close