package hub_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "sort" "testing" "time" beszelTests "github.com/henrygd/beszel/internal/tests" "github.com/henrygd/beszel/internal/migrations" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" pbTests "github.com/pocketbase/pocketbase/tests" "github.com/stretchr/testify/require" ) // marshal to json and return an io.Reader (for use in ApiScenario.Body) func jsonReader(v any) io.Reader { data, err := json.Marshal(v) if err != nil { panic(err) } return bytes.NewReader(data) } type gatedReader struct { data []byte started chan struct{} release chan struct{} offset int } func (r *gatedReader) Read(p []byte) (int, error) { if r.offset == 0 { close(r.started) <-r.release } if r.offset >= len(r.data) { return 0, io.EOF } n := copy(p, r.data[r.offset:]) r.offset += n return n, nil } func firstUserTestMux(t *testing.T) (*beszelTests.TestHub, http.Handler) { t.Helper() hub, err := beszelTests.NewTestHub(t.TempDir()) require.NoError(t, err) _ = hub.StartHub() router, err := apis.NewRouter(hub.TestApp) require.NoError(t, err) serveEvent := &core.ServeEvent{App: hub.TestApp, Router: router} var handler http.Handler err = hub.TestApp.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error { var buildErr error handler, buildErr = e.Router.BuildMux() return buildErr }) require.NoError(t, err) require.NotNil(t, handler) return hub, handler } func postFirstUser(handler http.Handler, email string) *httptest.ResponseRecorder { body, _ := json.Marshal(map[string]string{ "email": email, "password": "password123", }) req := httptest.NewRequest(http.MethodPost, "/api/beszel/create-user", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) return recorder } func TestApiRoutesAuthentication(t *testing.T) { hub, user := beszelTests.GetHubWithUser(t) defer hub.Cleanup() userToken, err := user.NewAuthToken() require.NoError(t, err, "Failed to create auth token") // Create test user and get auth token user2, err := beszelTests.CreateUser(hub, "testuser@example.com", "password123") require.NoError(t, err, "Failed to create test user") user2Token, err := user2.NewAuthToken() require.NoError(t, err, "Failed to create user2 auth token") adminUser, err := beszelTests.CreateUserWithRole(hub, "admin@example.com", "password123", "admin") require.NoError(t, err, "Failed to create admin user") adminUserToken, err := adminUser.NewAuthToken() readOnlyUser, err := beszelTests.CreateUserWithRole(hub, "readonly@example.com", "password123", "readonly") require.NoError(t, err, "Failed to create readonly user") readOnlyUserToken, err := readOnlyUser.NewAuthToken() require.NoError(t, err, "Failed to create readonly user auth token") superuser, err := beszelTests.CreateSuperuser(hub, "superuser@example.com", "password123") require.NoError(t, err, "Failed to create superuser") superuserToken, err := superuser.NewAuthToken() require.NoError(t, err, "Failed to create superuser auth token") // Create test system system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{ "name": "test-system", "users": []string{user.Id, readOnlyUser.Id}, "host": "127.0.0.1", }) require.NoError(t, err, "Failed to create test system") testAppFactory := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenarios := []beszelTests.ApiScenario{ // Auth Protected Routes - Should require authentication { Name: "GET /config-yaml - no auth should fail", Method: http.MethodGet, URL: "/api/beszel/config-yaml", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /config-yaml - with user auth should fail", Method: http.MethodGet, URL: "/api/beszel/config-yaml", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 403, ExpectedContent: []string{"The authorized record is not allowed to perform this action."}, TestAppFactory: testAppFactory, }, { Name: "GET /config-yaml - with admin auth should succeed", Method: http.MethodGet, URL: "/api/beszel/config-yaml", Headers: map[string]string{ "Authorization": adminUserToken, }, ExpectedStatus: 200, ExpectedContent: []string{"test-system"}, TestAppFactory: testAppFactory, }, { Name: "GET /heartbeat-status - no auth should fail", Method: http.MethodGet, URL: "/api/beszel/heartbeat-status", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /heartbeat-status - with user auth should fail", Method: http.MethodGet, URL: "/api/beszel/heartbeat-status", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 403, ExpectedContent: []string{"The authorized record is not allowed to perform this action."}, TestAppFactory: testAppFactory, }, { Name: "GET /heartbeat-status - with admin auth should succeed", Method: http.MethodGet, URL: "/api/beszel/heartbeat-status", Headers: map[string]string{ "Authorization": adminUserToken, }, ExpectedStatus: 200, ExpectedContent: []string{`"enabled":false`}, TestAppFactory: testAppFactory, }, { Name: "POST /test-heartbeat - with user auth should fail", Method: http.MethodPost, URL: "/api/beszel/test-heartbeat", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 403, ExpectedContent: []string{"The authorized record is not allowed to perform this action."}, TestAppFactory: testAppFactory, }, { Name: "POST /test-heartbeat - with admin auth should report disabled state", Method: http.MethodPost, URL: "/api/beszel/test-heartbeat", Headers: map[string]string{ "Authorization": adminUserToken, }, ExpectedStatus: 200, ExpectedContent: []string{"Heartbeat not configured"}, TestAppFactory: testAppFactory, }, { Name: "GET /universal-token - no auth should fail", Method: http.MethodGet, URL: "/api/beszel/universal-token", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /universal-token - with auth should succeed", Method: http.MethodGet, URL: "/api/beszel/universal-token", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"active", "token", "permanent"}, TestAppFactory: testAppFactory, }, { Name: "GET /universal-token - enable permanent should succeed", Method: http.MethodGet, URL: "/api/beszel/universal-token?enable=1&permanent=1&token=permanent-token-123", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"permanent\":true", "permanent-token-123"}, TestAppFactory: testAppFactory, }, { Name: "GET /universal-token - superuser should fail", Method: http.MethodGet, URL: "/api/beszel/universal-token", Headers: map[string]string{ "Authorization": superuserToken, }, ExpectedStatus: 403, ExpectedContent: []string{"Superusers cannot use universal tokens"}, TestAppFactory: func(t testing.TB) *pbTests.TestApp { return hub.TestApp }, }, { Name: "GET /universal-token - with readonly auth should fail", Method: http.MethodGet, URL: "/api/beszel/universal-token", Headers: map[string]string{ "Authorization": readOnlyUserToken, }, ExpectedStatus: 403, ExpectedContent: []string{"The authorized record is not allowed to perform this action."}, TestAppFactory: testAppFactory, }, { Name: "POST /smart/refresh - missing system should fail 400 with user auth", Method: http.MethodPost, URL: "/api/beszel/smart/refresh", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "system", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "POST /smart/refresh - with readonly auth should fail", Method: http.MethodPost, URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id), Headers: map[string]string{ "Authorization": readOnlyUserToken, }, ExpectedStatus: 403, ExpectedContent: []string{"The authorized record is not allowed to perform this action."}, TestAppFactory: testAppFactory, }, { Name: "POST /smart/refresh - non-user system should fail", Method: http.MethodPost, URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id), Headers: map[string]string{ "Authorization": user2Token, }, ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, }, { Name: "POST /smart/refresh - good user should pass validation", Method: http.MethodPost, URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id), Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 500, ExpectedContent: []string{"Something went wrong while processing your request."}, TestAppFactory: testAppFactory, }, { Name: "POST /user-alerts - no auth should fail", Method: http.MethodPost, URL: "/api/beszel/user-alerts", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "value": 80, "min": 10, "systems": []string{system.Id}, }), }, { Name: "POST /user-alerts - with auth should succeed", Method: http.MethodPost, URL: "/api/beszel/user-alerts", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"success\":true"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "value": 80, "min": 10, "systems": []string{system.Id}, }), }, { Name: "POST /user-alerts - readonly user can create own alert", Method: http.MethodPost, URL: "/api/beszel/user-alerts", Headers: map[string]string{ "Authorization": readOnlyUserToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"success\":true"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "value": 80, "min": 10, "systems": []string{system.Id}, }), AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { alerts, _ := app.CountRecords("alerts", dbx.HashExp{"user": readOnlyUser.Id}) require.EqualValues(t, 1, alerts) }, }, { Name: "DELETE /user-alerts - no auth should fail", Method: http.MethodDelete, URL: "/api/beszel/user-alerts", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "systems": []string{system.Id}, }), }, { Name: "DELETE /user-alerts - with auth should succeed", Method: http.MethodDelete, URL: "/api/beszel/user-alerts", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"success\":true"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "systems": []string{system.Id}, }), BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { // Create an alert to delete beszelTests.CreateRecord(app, "alerts", map[string]any{ "name": "CPU", "system": system.Id, "user": user.Id, "value": 80, "min": 10, }) }, }, { Name: "DELETE /user-alerts - readonly user can delete own alert", Method: http.MethodDelete, URL: "/api/beszel/user-alerts", Headers: map[string]string{ "Authorization": readOnlyUserToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"count\":1", "\"success\":true"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "systems": []string{system.Id}, }), BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { beszelTests.CreateRecord(app, "alerts", map[string]any{ "name": "CPU", "system": system.Id, "user": readOnlyUser.Id, "value": 80, }) }, AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { alerts, _ := app.CountRecords("alerts", dbx.HashExp{"user": readOnlyUser.Id}) require.Zero(t, alerts) }, }, { Name: "GET /containers/logs - no auth should fail", Method: http.MethodGet, URL: "/api/beszel/containers/logs?system=test-system&container=abababababab", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/logs - request for valid non-user system should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/containers/logs?system=%s&container=abababababab", system.Id), ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, Headers: map[string]string{ "Authorization": user2Token, }, }, { Name: "GET /containers/info - request for valid non-user system should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/containers/info?system=%s&container=abababababab", system.Id), ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, Headers: map[string]string{ "Authorization": user2Token, }, }, { Name: "GET /containers/info - SHARE_ALL_SYSTEMS allows non-member user", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/containers/info?system=%s&container=abababababab", system.Id), ExpectedStatus: 500, ExpectedContent: []string{"Something went wrong while processing your request."}, TestAppFactory: testAppFactory, Headers: map[string]string{ "Authorization": user2Token, }, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { t.Setenv("SHARE_ALL_SYSTEMS", "true") }, AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { t.Setenv("SHARE_ALL_SYSTEMS", "") }, }, { Name: "GET /containers/logs - with auth but missing system param should fail", Method: http.MethodGet, URL: "/api/beszel/containers/logs?container=abababababab", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/logs - with auth but missing container param should fail", Method: http.MethodGet, URL: "/api/beszel/containers/logs?system=test-system", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/logs - with auth but invalid system should fail", Method: http.MethodGet, URL: "/api/beszel/containers/logs?system=invalid-system&container=0123456789ab", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/logs - traversal container should fail validation", Method: http.MethodGet, URL: "/api/beszel/containers/logs?system=" + system.Id + "&container=..%2F..%2Fversion", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/info - traversal container should fail validation", Method: http.MethodGet, URL: "/api/beszel/containers/info?system=" + system.Id + "&container=../../version?x=", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/info - non-hex container should fail validation", Method: http.MethodGet, URL: "/api/beszel/containers/info?system=" + system.Id + "&container=container_name", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/logs - good user should pass validation", Method: http.MethodGet, URL: "/api/beszel/containers/logs?system=" + system.Id + "&container=0123456789ab", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 500, ExpectedContent: []string{"Something went wrong while processing your request."}, TestAppFactory: testAppFactory, }, { Name: "GET /containers/info - good user should pass validation", Method: http.MethodGet, URL: "/api/beszel/containers/info?system=" + system.Id + "&container=0123456789ab", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 500, ExpectedContent: []string{"Something went wrong while processing your request."}, TestAppFactory: testAppFactory, }, // /systemd routes { Name: "GET /systemd/info - no auth should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id), ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /systemd/info - request for valid non-user system should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id), ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, Headers: map[string]string{ "Authorization": user2Token, }, }, { Name: "GET /systemd/info - with auth but missing system param should fail", Method: http.MethodGet, URL: "/api/beszel/systemd/info?service=nginx.service", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /systemd/info - with auth but missing service param should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s", system.Id), Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 400, ExpectedContent: []string{"Invalid", "parameter"}, TestAppFactory: testAppFactory, }, { Name: "GET /systemd/info - with auth but invalid system should fail", Method: http.MethodGet, URL: "/api/beszel/systemd/info?system=invalid-system&service=nginx.service", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, }, { Name: "GET /systemd/info - service not in systemd_services collection should fail", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=notregistered.service", system.Id), Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 404, ExpectedContent: []string{"The requested resource wasn't found."}, TestAppFactory: testAppFactory, }, { Name: "GET /systemd/info - with auth and existing service record should pass validation", Method: http.MethodGet, URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id), Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 500, ExpectedContent: []string{"Something went wrong while processing your request."}, TestAppFactory: testAppFactory, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { beszelTests.CreateRecord(app, "systemd_services", map[string]any{ "system": system.Id, "name": "nginx.service", "state": 0, "sub": 1, }) }, }, // Auth Optional Routes - Should work without authentication { Name: "GET /getkey - no auth should fail", Method: http.MethodGet, URL: "/api/beszel/getkey", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /getkey - with auth should also succeed", Method: http.MethodGet, URL: "/api/beszel/getkey", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"key\":", "\"v\":"}, TestAppFactory: testAppFactory, }, { Name: "GET /info - should return the same as /getkey", Method: http.MethodGet, URL: "/api/beszel/info", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"key\":", "\"v\":"}, TestAppFactory: testAppFactory, }, { Name: "GET /first-run - no auth should succeed", Method: http.MethodGet, URL: "/api/beszel/first-run", ExpectedStatus: 200, ExpectedContent: []string{"\"firstRun\":false"}, TestAppFactory: testAppFactory, }, { Name: "GET /first-run - with auth should also succeed", Method: http.MethodGet, URL: "/api/beszel/first-run", Headers: map[string]string{ "Authorization": userToken, }, ExpectedStatus: 200, ExpectedContent: []string{"\"firstRun\":false"}, TestAppFactory: testAppFactory, }, { Name: "GET /agent-connect - no auth should succeed (websocket upgrade fails but route is accessible)", Method: http.MethodGet, URL: "/api/beszel/agent-connect", ExpectedStatus: 400, ExpectedContent: []string{}, TestAppFactory: testAppFactory, }, { Name: "POST /test-notification - invalid auth token should fail", Method: http.MethodPost, URL: "/api/beszel/test-notification", Body: jsonReader(map[string]any{ "url": "generic://127.0.0.1", }), Headers: map[string]string{ "Authorization": "invalid-token", }, ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "POST /user-alerts - invalid auth token should fail", Method: http.MethodPost, URL: "/api/beszel/user-alerts", Headers: map[string]string{ "Authorization": "invalid-token", }, ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, Body: jsonReader(map[string]any{ "name": "CPU", "value": 80, "min": 10, "systems": []string{system.Id}, }), }, // this works but diff behavior on prod vs dev. // dev returns 502; prod returns 200 with static html page 404 // TODO: align dev and prod behavior and re-enable this test // { // Name: "GET /update - shouldn't exist without CHECK_UPDATES env var", // Method: http.MethodGet, // URL: "/api/beszel/update", // NotExpectedContent: []string{"v:", "\"v\":"}, // ExpectedStatus: 502, // TestAppFactory: testAppFactory, // }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestFirstUserCreation(t *testing.T) { t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) { hub, _ := beszelTests.NewTestHub(t.TempDir()) defer hub.Cleanup() hub.StartHub() testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenarios := []beszelTests.ApiScenario{ { Name: "POST /create-user - should be available when no users exist", Method: http.MethodPost, URL: "/api/beszel/create-user", Body: jsonReader(map[string]any{ "email": "firstuser@example.com", "password": "password123", }), ExpectedStatus: 200, ExpectedContent: []string{"User created"}, TestAppFactory: testAppFactoryExisting, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { userCount, err := hub.CountRecords("users") require.NoError(t, err) require.Zero(t, userCount, "Should start with no users") superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser") require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser") }, AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { userCount, err := hub.CountRecords("users") require.NoError(t, err) require.EqualValues(t, 1, userCount, "Should have created one user") superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.EqualValues(t, 1, len(superusers), "Should have created one superuser") require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser") }, }, { Name: "POST /create-user - should not be available when users exist", Method: http.MethodPost, URL: "/api/beszel/create-user", Body: jsonReader(map[string]any{ "email": "firstuser@example.com", "password": "password123", }), ExpectedStatus: 404, ExpectedContent: []string{"wasn't found"}, TestAppFactory: testAppFactoryExisting, }, } for _, scenario := range scenarios { scenario.Test(t) } }) t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) { t.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com") t.Setenv("BESZEL_HUB_USER_PASSWORD", "password123") hub, _ := beszelTests.NewTestHub(t.TempDir()) defer hub.Cleanup() hub.StartHub() testAppFactory := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenario := beszelTests.ApiScenario{ Name: "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set", Method: http.MethodPost, URL: "/api/beszel/create-user", ExpectedStatus: 404, ExpectedContent: []string{"wasn't found"}, TestAppFactory: testAppFactory, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { users, err := hub.FindAllRecords("users") require.NoError(t, err) require.EqualValues(t, 1, len(users), "Should start with one user") require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user") superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.EqualValues(t, 1, len(superusers), "Should start with one superuser") require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser") }, AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { users, err := hub.FindAllRecords("users") require.NoError(t, err) require.EqualValues(t, 1, len(users), "Should still have one user") require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user") superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.EqualValues(t, 1, len(superusers), "Should still have one superuser") require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser") }, } scenario.Test(t) }) } func TestFirstUserBootstrapAtomicity(t *testing.T) { t.Run("concurrent complete requests produce exactly one winner", func(t *testing.T) { hub, handler := firstUserTestMux(t) defer hub.Cleanup() start := make(chan struct{}) statuses := make(chan int, 2) for _, email := range []string{"first@example.com", "second@example.com"} { go func(email string) { <-start statuses <- postFirstUser(handler, email).Code }(email) } close(start) got := []int{<-statuses, <-statuses} sort.Ints(got) require.Equal(t, []int{http.StatusOK, http.StatusForbidden}, got) users, err := hub.FindAllRecords("users") require.NoError(t, err) require.Len(t, users, 1) superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.Len(t, superusers, 1) require.NotEqual(t, migrations.TempAdminEmail, superusers[0].Email()) }) t.Run("partial body cannot retain stale bootstrap authorization", func(t *testing.T) { hub, handler := firstUserTestMux(t) defer hub.Cleanup() body, err := json.Marshal(map[string]string{ "email": "parked@example.com", "password": "password123", }) require.NoError(t, err) gated := &gatedReader{ data: body, started: make(chan struct{}), release: make(chan struct{}), } parkedRequest := httptest.NewRequest(http.MethodPost, "/api/beszel/create-user", gated) parkedRequest.Header.Set("Content-Type", "application/json") parkedRecorder := httptest.NewRecorder() parkedDone := make(chan struct{}) go func() { handler.ServeHTTP(parkedRecorder, parkedRequest) close(parkedDone) }() select { case <-gated.started: case <-time.After(2 * time.Second): t.Fatal("parked request did not begin reading its body") } operatorRecorder := postFirstUser(handler, "operator@example.com") require.Equal(t, http.StatusOK, operatorRecorder.Code) lateRecorder := postFirstUser(handler, "late@example.com") require.Equal(t, http.StatusForbidden, lateRecorder.Code) close(gated.release) select { case <-parkedDone: case <-time.After(2 * time.Second): t.Fatal("parked request did not finish") } require.Equal(t, http.StatusForbidden, parkedRecorder.Code) users, err := hub.FindAllRecords("users") require.NoError(t, err) require.Len(t, users, 1) require.Equal(t, "operator@example.com", users[0].Email()) superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) require.NoError(t, err) require.Len(t, superusers, 1) require.Equal(t, "operator@example.com", superusers[0].Email()) }) } func TestCreateUserEndpointAvailability(t *testing.T) { t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) { hub, _ := beszelTests.NewTestHub(t.TempDir()) defer hub.Cleanup() // Ensure no users exist userCount, err := hub.CountRecords("users") require.NoError(t, err) require.Zero(t, userCount, "Should start with no users") hub.StartHub() testAppFactory := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenario := beszelTests.ApiScenario{ Name: "POST /create-user - should be available when no users exist", Method: http.MethodPost, URL: "/api/beszel/create-user", Body: jsonReader(map[string]any{ "email": "firstuser@example.com", "password": "password123", }), ExpectedStatus: 200, ExpectedContent: []string{"User created"}, TestAppFactory: testAppFactory, } scenario.Test(t) // Verify user was created userCount, err = hub.CountRecords("users") require.NoError(t, err) require.EqualValues(t, 1, userCount, "Should have created one user") }) t.Run("CreateUserEndpoint not available when users exist", func(t *testing.T) { hub, _ := beszelTests.NewTestHub(t.TempDir()) defer hub.Cleanup() // Create a user first _, err := beszelTests.CreateUser(hub, "existing@example.com", "password") require.NoError(t, err) hub.StartHub() testAppFactory := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenario := beszelTests.ApiScenario{ Name: "POST /create-user - should not be available when users exist", Method: http.MethodPost, URL: "/api/beszel/create-user", Body: jsonReader(map[string]any{ "email": "another@example.com", "password": "password123", }), ExpectedStatus: 404, ExpectedContent: []string{"wasn't found"}, TestAppFactory: testAppFactory, } scenario.Test(t) }) } func TestAutoLoginMiddleware(t *testing.T) { var hubs []*beszelTests.TestHub defer func() { for _, hub := range hubs { hub.Cleanup() } }() t.Setenv("AUTO_LOGIN", "user@test.com") testAppFactory := func(t testing.TB) *pbTests.TestApp { hub, _ := beszelTests.NewTestHub(t.TempDir()) hubs = append(hubs, hub) hub.StartHub() return hub.TestApp } scenarios := []beszelTests.ApiScenario{ { Name: "GET /getkey - without auto login should fail", Method: http.MethodGet, URL: "/api/beszel/getkey", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /getkey - with auto login should fail if no matching user", Method: http.MethodGet, URL: "/api/beszel/getkey", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /getkey - with auto login should succeed", Method: http.MethodGet, URL: "/api/beszel/getkey", ExpectedStatus: 200, ExpectedContent: []string{"\"key\":", "\"v\":"}, TestAppFactory: testAppFactory, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { beszelTests.CreateUser(app, "user@test.com", "password123") }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestTrustedHeaderMiddleware(t *testing.T) { var hubs []*beszelTests.TestHub defer func() { for _, hub := range hubs { hub.Cleanup() } }() t.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted") testAppFactory := func(t testing.TB) *pbTests.TestApp { hub, _ := beszelTests.NewTestHub(t.TempDir()) hubs = append(hubs, hub) hub.StartHub() return hub.TestApp } scenarios := []beszelTests.ApiScenario{ { Name: "GET /getkey - without trusted header should fail", Method: http.MethodGet, URL: "/api/beszel/getkey", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /getkey - with trusted header should fail if no matching user", Method: http.MethodGet, URL: "/api/beszel/getkey", Headers: map[string]string{ "X-Beszel-Trusted": "user@test.com", }, ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, { Name: "GET /getkey - with trusted header should succeed", Method: http.MethodGet, URL: "/api/beszel/getkey", Headers: map[string]string{ "X-Beszel-Trusted": "user@test.com", }, ExpectedStatus: 200, ExpectedContent: []string{"\"key\":", "\"v\":"}, TestAppFactory: testAppFactory, BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { beszelTests.CreateUser(app, "user@test.com", "password123") }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUpdateEndpoint(t *testing.T) { t.Setenv("CHECK_UPDATES", "true") hub, _ := beszelTests.NewTestHub(t.TempDir()) defer hub.Cleanup() hub.StartHub() // Create test user and get auth token // user, err := beszelTests.CreateUser(hub, "testuser@example.com", "password123") // require.NoError(t, err, "Failed to create test user") // userToken, err := user.NewAuthToken() testAppFactory := func(t testing.TB) *pbTests.TestApp { return hub.TestApp } scenarios := []beszelTests.ApiScenario{ { Name: "update endpoint shouldn't work without auth", Method: http.MethodGet, URL: "/api/beszel/update", ExpectedStatus: 401, ExpectedContent: []string{"requires valid"}, TestAppFactory: testAppFactory, }, // leave this out for now since it actually makes a request to github // { // Name: "GET /update - with valid auth should succeed", // Method: http.MethodGet, // URL: "/api/beszel/update", // Headers: map[string]string{ // "Authorization": userToken, // }, // ExpectedStatus: 200, // ExpectedContent: []string{`"v":`}, // TestAppFactory: testAppFactory, // }, } for _, scenario := range scenarios { scenario.Test(t) } }