v3.3.1 31f1eec

local_admin_socket_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"io"
6	"net"
7	"net/http"
8	"net/http/httptest"
9	"os"
10	"path/filepath"
11	"testing"
12	"time"
13
14	"github.com/bnema/zerowrap"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/mock"
17	"github.com/stretchr/testify/require"
18
19	adminhttp "github.com/bnema/gordon/internal/adapters/in/http/admin"
20	"github.com/bnema/gordon/internal/adapters/localadmin"
21	in "github.com/bnema/gordon/internal/boundaries/in"
22	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
23)
24
25// unixHTTPClient returns a client that dials the given Unix socket and never
26// consults environment proxies.
27func unixHTTPClient(socketPath string) *http.Client {
28	dialer := &net.Dialer{Timeout: 5 * time.Second}
29	return &http.Client{
30		Timeout: 10 * time.Second,
31		Transport: &http.Transport{
32			Proxy: nil,
33			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
34				return dialer.DialContext(ctx, "unix", socketPath)
35			},
36		},
37		CheckRedirect: func(*http.Request, []*http.Request) error {
38			return http.ErrUseLastResponse
39		},
40	}
41}
42
43func localAdminTestServices(t *testing.T, appSvc in.AppService) *services {
44	t.Helper()
45	return &services{adminHandler: adminhttp.NewHandler(adminhttp.HandlerDeps{
46		AppSvc: appSvc,
47		Log:    zerowrap.Default(),
48	})}
49}
50
51func TestLocalAdminSocketServesAppSurfaceOverUnix(t *testing.T) {
52	xdg := t.TempDir()
53	t.Setenv("XDG_RUNTIME_DIR", xdg)
54
55	appSvc := inmocks.NewMockAppService(t)
56	appSvc.EXPECT().List(mock.Anything).Return([]in.AppSummary(nil), nil).Once()
57
58	server, err := startLocalAdminServer(localAdminTestServices(t, appSvc), make(chan error, 4), zerowrap.Default())
59	require.NoError(t, err)
60	require.NotNil(t, server)
61
62	path := localadmin.SocketPath(filepath.Join(xdg, "gordon"))
63	require.NoError(t, localadmin.ValidateSocket(path))
64
65	client := unixHTTPClient(path)
66
67	resp, err := client.Get("http://gordon.local/admin/apps")
68	require.NoError(t, err)
69	body, err := io.ReadAll(resp.Body)
70	require.NoError(t, err)
71	resp.Body.Close()
72	require.Equal(t, http.StatusOK, resp.StatusCode, "body: %s", body)
73
74	resp, err = client.Get("http://gordon.local/admin/auth/tokens")
75	require.NoError(t, err)
76	resp.Body.Close()
77	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
78
79	server.Close()
80	assert.NoFileExists(t, path)
81}
82
83func TestLocalAdminSocketTCPAdminStaysDisabled(t *testing.T) {
84	xdg := t.TempDir()
85	t.Setenv("XDG_RUNTIME_DIR", xdg)
86
87	cfg := Config{}
88	cfg.Auth.Enabled = false
89
90	svc := localAdminTestServices(t, nil)
91	registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
92
93	// The TCP registry mux must not expose /admin/* in local-only mode.
94	for _, path := range []string{"/admin/status", "/admin/apps", "/admin/apps/apply"} {
95		req := httptest.NewRequest(http.MethodGet, path, nil)
96		req.RemoteAddr = "192.0.2.10:12345"
97		rec := httptest.NewRecorder()
98		registryHandler.ServeHTTP(rec, req)
99		require.Equal(t, http.StatusNotFound, rec.Code, "path %s", path)
100	}
101
102	// The owner-only socket does serve the app surface.
103	server, err := startLocalAdminServer(svc, make(chan error, 4), zerowrap.Default())
104	require.NoError(t, err)
105	t.Cleanup(server.Close)
106
107	path := localadmin.SocketPath(filepath.Join(xdg, "gordon"))
108	client := unixHTTPClient(path)
109	resp, err := client.Get("http://gordon.local/admin/apps")
110	require.NoError(t, err)
111	resp.Body.Close()
112	assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
113}
114
115func TestLocalAdminSocketCloseLeavesReplacementFile(t *testing.T) {
116	xdg := t.TempDir()
117	t.Setenv("XDG_RUNTIME_DIR", xdg)
118
119	server, err := startLocalAdminServer(localAdminTestServices(t, inmocks.NewMockAppService(t)), make(chan error, 4), zerowrap.Default())
120	require.NoError(t, err)
121
122	path := localadmin.SocketPath(filepath.Join(xdg, "gordon"))
123	require.NoError(t, os.Remove(path))
124	require.NoError(t, os.WriteFile(path, []byte("replacement"), 0o600))
125
126	server.Close()
127
128	content, err := os.ReadFile(path)
129	require.NoError(t, err)
130	assert.Equal(t, "replacement", string(content))
131
132	// Close is idempotent and must not remove the replacement on a second call.
133	server.Close()
134	assert.FileExists(t, path)
135}
136
137func TestStartLocalAdminServerSkipsWithoutAdminHandler(t *testing.T) {
138	server, err := startLocalAdminServer(&services{}, make(chan error, 4), zerowrap.Default())
139	require.NoError(t, err)
140	assert.Nil(t, server)
141	server.Close()
142}

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 7

Tags 106

↑↓ moveenter openesc close