mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-09-26 19:51:26 +08:00
@@ -260,7 +260,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
a.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
|
@@ -164,7 +164,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
m.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
|
@@ -132,7 +132,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
||||
s.Log(logger.Info, "connection %v failed to authenticate: %v",
|
||||
httpp.RemoteAddr(ctx), err.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
|
@@ -110,7 +110,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
pp.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
|
@@ -164,7 +164,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
||||
|
||||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
|
@@ -13,9 +13,11 @@ import (
|
||||
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/stream"
|
||||
"github.com/bluenviron/mediamtx/internal/test"
|
||||
"github.com/bluenviron/mediamtx/internal/unit"
|
||||
@@ -500,3 +502,66 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestAuthError(t *testing.T) {
|
||||
n := 0
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AlwaysRemux: true,
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.Duration(1 * time.Second),
|
||||
PartDuration: conf.Duration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
PathManager: &dummyPathManager{
|
||||
findPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
|
||||
return nil, &auth.Error{AskCredentials: true}
|
||||
}
|
||||
|
||||
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
|
||||
},
|
||||
},
|
||||
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
|
||||
if l == logger.Info {
|
||||
if n == 1 {
|
||||
require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
|
||||
}
|
||||
n++
|
||||
}
|
||||
}),
|
||||
}
|
||||
err := s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/stream/index.m3u8", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/stream/index.m3u8", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Greater(t, time.Since(start), 2*time.Second)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
|
||||
require.Equal(t, 2, n)
|
||||
}
|
||||
|
@@ -168,7 +168,7 @@ func (c *conn) runRead() error {
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return terr
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func (c *conn) runPublish() error {
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return terr
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/bluenviron/gortsplib/v5"
|
||||
rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/headers"
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -37,12 +36,6 @@ func absoluteURL(req *base.Request, v string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func credentialsProvided(req *base.Request) bool {
|
||||
var auth headers.Authorization
|
||||
err := auth.Unmarshal(req.Header["Authorization"])
|
||||
return err == nil && auth.Username != ""
|
||||
}
|
||||
|
||||
func tunnelLabel(t gortsplib.Tunnel) string {
|
||||
switch t {
|
||||
case gortsplib.TunnelHTTP:
|
||||
@@ -175,7 +168,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||
if res.Err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(res.Err, &terr) {
|
||||
res, err2 := c.handleAuthError(ctx.Request)
|
||||
res, err2 := c.handleAuthError(terr)
|
||||
return res, nil, err2
|
||||
}
|
||||
|
||||
@@ -212,17 +205,19 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||
}, stream, nil
|
||||
}
|
||||
|
||||
func (c *conn) handleAuthError(req *base.Request) (*base.Response, error) {
|
||||
if credentialsProvided(req) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
}
|
||||
|
||||
// let gortsplib decide whether connection should be terminated,
|
||||
// depending on whether credentials have been provided or not.
|
||||
func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {
|
||||
if err.AskCredentials {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
}, liberrors.ErrServerAuth{}
|
||||
}
|
||||
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c *conn) apiItem() *defs.APIRTSPConn {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/stream"
|
||||
"github.com/bluenviron/mediamtx/internal/test"
|
||||
"github.com/bluenviron/mediamtx/internal/unit"
|
||||
@@ -393,3 +396,54 @@ func TestServerRedirect(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthError(t *testing.T) {
|
||||
pathManager := &test.PathManager{
|
||||
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
|
||||
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
|
||||
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true}}
|
||||
}
|
||||
|
||||
return defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf("auth error")}}
|
||||
},
|
||||
}
|
||||
|
||||
n := new(int64)
|
||||
done := make(chan struct{})
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8557",
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
WriteTimeout: conf.Duration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: pathManager,
|
||||
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
|
||||
if l == logger.Info {
|
||||
if atomic.AddInt64(n, 1) == 3 {
|
||||
require.Regexp(t, "authentication failed: auth error$", fmt.Sprintf(s, i...))
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
err := s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
reader := gortsplib.Client{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
err = reader.Start()
|
||||
require.NoError(t, err)
|
||||
defer reader.Close()
|
||||
|
||||
_, _, err = reader.Describe(u)
|
||||
require.EqualError(t, err, "bad status code: 401 (Unauthorized)")
|
||||
|
||||
<-done
|
||||
}
|
||||
|
@@ -179,7 +179,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
return c.handleAuthError(ctx.Request)
|
||||
return c.handleAuthError(terr)
|
||||
}
|
||||
|
||||
return &base.Response{
|
||||
@@ -242,7 +242,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
res, err2 := c.handleAuthError(ctx.Request)
|
||||
res, err2 := c.handleAuthError(terr)
|
||||
return res, nil, err2
|
||||
}
|
||||
|
||||
|
@@ -148,7 +148,7 @@ func (c *conn) runPublish(streamID *streamID) error {
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
c.connReq.Reject(srt.REJ_PEER)
|
||||
return terr
|
||||
@@ -272,7 +272,7 @@ func (c *conn) runRead(streamID *streamID) error {
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
c.connReq.Reject(srt.REJ_PEER)
|
||||
return terr
|
||||
|
@@ -141,7 +141,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
|
||||
|
||||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
writeError(ctx, http.StatusUnauthorized, terr)
|
||||
@@ -203,7 +203,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
|
||||
|
||||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
// wait some seconds to delay brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
writeError(ctx, http.StatusUnauthorized, terr)
|
||||
|
@@ -3,6 +3,7 @@ package webrtc
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -12,9 +13,11 @@ import (
|
||||
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
|
||||
"github.com/bluenviron/mediamtx/internal/protocols/whip"
|
||||
"github.com/bluenviron/mediamtx/internal/stream"
|
||||
@@ -63,9 +66,6 @@ func initializeTestServer(t *testing.T) *Server {
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "*",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
@@ -78,7 +78,6 @@ func initializeTestServer(t *testing.T) *Server {
|
||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
||||
ExternalCmdPool: nil,
|
||||
PathManager: pm,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
@@ -148,10 +147,6 @@ func TestServerOptionsICEServer(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
@@ -167,7 +162,6 @@ func TestServerOptionsICEServer(t *testing.T) {
|
||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
||||
ExternalCmdPool: nil,
|
||||
PathManager: pathManager,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
@@ -234,10 +228,6 @@ func TestServerPublish(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
@@ -249,7 +239,6 @@ func TestServerPublish(t *testing.T) {
|
||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
||||
ExternalCmdPool: nil,
|
||||
PathManager: pathManager,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
@@ -523,11 +512,6 @@ func TestServerRead(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
LocalTCPAddress: "127.0.0.1:8887",
|
||||
@@ -538,7 +522,6 @@ func TestServerRead(t *testing.T) {
|
||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
||||
ExternalCmdPool: nil,
|
||||
PathManager: pathManager,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
@@ -612,10 +595,6 @@ func TestServerReadNotFound(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
@@ -627,7 +606,6 @@ func TestServerReadNotFound(t *testing.T) {
|
||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
||||
ExternalCmdPool: nil,
|
||||
PathManager: pm,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
@@ -754,3 +732,57 @@ func TestICEServerClientOnly(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, serverICEServers)
|
||||
}
|
||||
|
||||
func TestAuthError(t *testing.T) {
|
||||
n := 0
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8886",
|
||||
ReadTimeout: conf.Duration(10 * time.Second),
|
||||
PathManager: &test.PathManager{
|
||||
FindPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
|
||||
return nil, &auth.Error{AskCredentials: true}
|
||||
}
|
||||
|
||||
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
|
||||
},
|
||||
},
|
||||
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
|
||||
if l == logger.Info {
|
||||
if n == 1 {
|
||||
require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
|
||||
}
|
||||
n++
|
||||
}
|
||||
}),
|
||||
}
|
||||
err := s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8886/stream/publish", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8886/stream/publish", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Greater(t, time.Since(start), 2*time.Second)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
|
||||
require.Equal(t, 2, n)
|
||||
}
|
||||
|
Reference in New Issue
Block a user