Files
mediamtx/internal/api/api_admin_test.go
T
QiuSW 9068712bb1
lint / go (push) Canceled after 0s
lint / go_mod (push) Canceled after 0s
lint / conf (push) Canceled after 0s
lint / docslinks (push) Canceled after 0s
lint / docsorder (push) Canceled after 0s
lint / apidocs (push) Canceled after 0s
lint / other (push) Canceled after 0s
test / test_64 (push) Canceled after 0s
test / test_32 (push) Canceled after 0s
test / test_e2e (push) Canceled after 0s
feat(admin): add complete device pagination (#6)
2026-08-28 16:48:17 +08:00

166 lines
5.7 KiB
Go

package api //nolint:revive
import (
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
)
const adminTestAddress = "localhost:9996"
func newAdminTestAPI(t *testing.T, authManager apiAuthManager) *API {
t.Helper()
api := &API{
Address: adminTestAddress,
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: authManager,
Parent: &testParent{},
}
require.NoError(t, api.Initialize())
t.Cleanup(api.Close)
return api
}
func readAdminResponse(t *testing.T, client *http.Client, url string) (*http.Response, string) {
t.Helper()
res, err := client.Get(url)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
return res, string(body)
}
func TestAdminAssets(t *testing.T) {
authCalls := 0
newAdminTestAPI(t, &test.AuthManager{
AuthenticateImpl: func(_ *auth.Request) (string, *auth.Error) {
authCalls++
return "", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("denied")}
},
})
transport := &http.Transport{}
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{Transport: transport}
res, body := readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/")
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "text/html; charset=utf-8", res.Header.Get("Content-Type"))
require.Equal(t, "no-store", res.Header.Get("Cache-Control"))
require.Contains(t, res.Header.Get("Content-Security-Policy"), "object-src 'none'")
require.Equal(t, "nosniff", res.Header.Get("X-Content-Type-Options"))
require.Contains(t, body, "MediaMTX Stream Console")
require.Contains(t, body, `id="login-form"`)
require.Contains(t, body, `id="logout-button"`)
require.Contains(t, body, "./app.css")
require.Contains(t, body, "./list-tools.js")
require.Contains(t, body, "./app.js")
require.NotContains(t, body, "https://")
require.Zero(t, authCalls)
res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/app.css")
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "text/css; charset=utf-8", res.Header.Get("Content-Type"))
require.Contains(t, body, "prefers-reduced-motion")
require.Contains(t, body, "@media (max-width: 700px)")
res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/app.js")
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "text/javascript; charset=utf-8", res.Header.Get("Content-Type"))
require.Contains(t, body, "sourceMasked")
require.Contains(t, body, "encodePathName")
require.Contains(t, body, "if (source) payload.source = source")
require.Contains(t, body, "redactSensitiveText")
require.Contains(t, body, "state.global.webrtcAddress")
require.Contains(t, body, "sessionStorage")
require.Contains(t, body, "Authorization")
require.NotContains(t, body, ".innerHTML")
require.NotContains(t, body, "localStorage")
res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/list-tools.js")
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "text/javascript; charset=utf-8", res.Header.Get("Content-Type"))
require.Contains(t, body, "fetchAllPages")
require.Contains(t, body, "API 返回了无效的分页数据")
require.Contains(t, body, "concurrency")
require.NotContains(t, body, ".innerHTML")
res, _ = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/missing.js")
require.Equal(t, http.StatusNotFound, res.StatusCode)
}
func TestAdminRedirect(t *testing.T) {
newAdminTestAPI(t, test.NilAuthManager)
transport := &http.Transport{}
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{
Transport: transport,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
res, _ := readAdminResponse(t, client, "http://"+adminTestAddress+"/admin")
require.Equal(t, http.StatusMovedPermanently, res.StatusCode)
require.Equal(t, "/admin/", res.Header.Get("Location"))
}
func TestAdminAPIUsesAuthentication(t *testing.T) {
var got *auth.Request
newAdminTestAPI(t, &test.AuthManager{
AuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {
got = req
return "", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("denied")}
},
})
transport := &http.Transport{}
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{Transport: transport}
res, body := readAdminResponse(t, client, "http://"+adminTestAddress+"/v3/info")
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
require.NotNil(t, got)
require.Equal(t, conf.AuthActionAPI, got.Action)
require.True(t, got.EnableAskCredentials)
require.True(t, strings.Contains(body, "authentication error"))
}
func TestAdminAPIAcceptsBasicCredentials(t *testing.T) {
newAdminTestAPI(t, &test.AuthManager{
AuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {
require.Equal(t, conf.AuthActionAPI, req.Action)
require.Equal(t, "test-admin", req.Credentials.User)
require.Equal(t, "test-password", req.Credentials.Pass)
return req.Credentials.User, nil
},
})
transport := &http.Transport{}
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{Transport: transport}
req, err := http.NewRequest(http.MethodGet, "http://"+adminTestAddress+"/v3/info", nil)
require.NoError(t, err)
req.SetBasicAuth("test-admin", "test-password")
res, err := client.Do(req)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusOK, res.StatusCode)
}