v0.25.1 7fabed9

interceptor_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6	"context"
7	"errors"
8	"net/http"
9	"testing"
10	"time"
11
12	"connectrpc.com/connect"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16
17	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
18	"bnema.dev/ermite/internal/domain"
19	"bnema.dev/ermite/internal/ports/mocks"
20)
21
22const peer = "203.0.113.7:4242"
23
24func bearerHeader(v string) http.Header {
25	h := http.Header{}
26	if v != "" {
27		h.Set("Authorization", v)
28	}
29	return h
30}
31
32func TestInterceptorAuthenticate(t *testing.T) {
33	t.Parallel()
34	const procedure = ermiteconnect.RepoServiceListReposProcedure
35	tests := []struct {
36		name      string
37		procedure string
38		header    http.Header
39		blocked   bool
40		authErr   error
41		wantCode  connect.Code // 0: success
42		wantAuth  bool
43		wantFail  bool
44		wantLimit bool // the limiter is asked at all
45		wantRetry string
46		wantOwner bool
47	}{
48		{name: "no header is anonymous", procedure: procedure, header: bearerHeader(""), wantLimit: true},
49		{name: "valid token", procedure: procedure, header: bearerHeader("Bearer good"), wantLimit: true, wantAuth: true, wantOwner: true},
50		{
51			name: "invalid token counts as a failure", procedure: procedure, header: bearerHeader("Bearer bad"),
52			authErr: domain.ErrUnauthenticated, wantCode: connect.CodeUnauthenticated, wantLimit: true, wantAuth: true, wantFail: true,
53		},
54		{
55			name: "non-bearer scheme is a failure", procedure: procedure, header: bearerHeader("Basic abc"),
56			authErr: domain.ErrUnauthenticated, wantCode: connect.CodeUnauthenticated, wantLimit: true, wantAuth: true, wantFail: true,
57		},
58		{
59			name: "authenticator outage is not a failure of the client", procedure: procedure, header: bearerHeader("Bearer good"),
60			authErr: errors.New("store down"), wantCode: connect.CodeInternal, wantLimit: true, wantAuth: true,
61		},
62		{
63			name: "blocked client is refused with Retry-After", procedure: procedure, header: bearerHeader("Bearer good"),
64			blocked: true, wantCode: connect.CodeResourceExhausted, wantLimit: true, wantRetry: "30",
65		},
66		{name: "version needs no authentication", procedure: ermiteconnect.ServerServiceGetVersionProcedure, header: bearerHeader("Bearer expired")},
67		{name: "signing key needs no authentication", procedure: ermiteconnect.ServerServiceGetSigningKeyProcedure, header: bearerHeader("Bearer expired")},
68	}
69	for _, tt := range tests {
70		t.Run(tt.name, func(t *testing.T) {
71			t.Parallel()
72			auth := mocks.NewAuthService(t)
73			limiter := mocks.NewFailureLimiter(t)
74			if tt.wantLimit {
75				wait := time.Duration(0)
76				if tt.blocked {
77					wait = 29*time.Second + 500*time.Millisecond
78				}
79				limiter.EXPECT().Blocked(mock.Anything, "203.0.113.7").Return(tt.blocked, wait).Once()
80			}
81			if tt.wantAuth {
82				p := domain.Principal{}
83				if tt.wantOwner {
84					p = domain.Owner()
85				}
86				auth.EXPECT().Authenticate(mock.Anything, mock.Anything).Return(p, tt.authErr).Once()
87			}
88			if tt.wantFail {
89				limiter.EXPECT().Fail(mock.Anything, "203.0.113.7").Once()
90			}
91			i := newInterceptor(Config{Auth: auth, Limiter: limiter})
92			ctx, err := i.authenticate(context.Background(), tt.procedure, peer, tt.header)
93			if tt.wantCode != 0 {
94				requireCode(t, tt.wantCode, err)
95				if tt.wantRetry != "" {
96					var ce *connect.Error
97					require.ErrorAs(t, err, &ce)
98					assert.Equal(t, tt.wantRetry, ce.Meta().Get("Retry-After"))
99				}
100				return
101			}
102			require.NoError(t, err)
103			p, ok := domain.PrincipalFromContext(ctx)
104			require.True(t, ok)
105			if tt.wantOwner {
106				assert.Equal(t, domain.Owner(), p)
107			} else {
108				assert.Equal(t, domain.Anonymous(), p)
109			}
110		})
111	}
112}
113
114func TestInterceptorIgnoresForwardedForFromUntrustedPeer(t *testing.T) {
115	t.Parallel()
116	limiter := mocks.NewFailureLimiter(t)
117	limiter.EXPECT().Blocked(mock.Anything, "203.0.113.7").Return(false, time.Duration(0)).Once()
118	h := bearerHeader("")
119	h.Set("X-Forwarded-For", "198.51.100.1")
120	i := newInterceptor(Config{Auth: mocks.NewAuthService(t), Limiter: limiter})
121	_, err := i.authenticate(context.Background(), ermiteconnect.RepoServiceListReposProcedure, peer, h)
122	require.NoError(t, err)
123}

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