454 lines
12 KiB
Go
454 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/mocks"
|
|
"github.com/authelia/authelia/v4/internal/model"
|
|
)
|
|
|
|
type FetchSuite struct {
|
|
suite.Suite
|
|
mock *mocks.MockAutheliaCtx
|
|
}
|
|
|
|
func (s *FetchSuite) SetupTest() {
|
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
|
// Set the initial user session.
|
|
userSession := s.mock.Ctx.GetSession()
|
|
userSession.Username = testUsername
|
|
userSession.AuthenticationLevel = 1
|
|
err := s.mock.Ctx.SaveSession(userSession)
|
|
require.NoError(s.T(), err)
|
|
}
|
|
|
|
func (s *FetchSuite) TearDownTest() {
|
|
s.mock.Close()
|
|
}
|
|
|
|
type expectedResponse struct {
|
|
db model.UserInfo
|
|
api *model.UserInfo
|
|
err error
|
|
}
|
|
|
|
type expectedResponseAlt struct {
|
|
description string
|
|
|
|
db model.UserInfo
|
|
api *model.UserInfo
|
|
loadErr error
|
|
saveErr error
|
|
config *schema.Configuration
|
|
}
|
|
|
|
func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
|
expectedResponses := []expectedResponse{
|
|
{
|
|
db: model.UserInfo{
|
|
Method: "totp",
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
db: model.UserInfo{
|
|
Method: "webauthn",
|
|
HasWebauthn: true,
|
|
HasTOTP: true,
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
db: model.UserInfo{
|
|
Method: "webauthn",
|
|
HasWebauthn: true,
|
|
HasTOTP: false,
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
db: model.UserInfo{
|
|
Method: "mobile_push",
|
|
HasWebauthn: false,
|
|
HasTOTP: false,
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
db: model.UserInfo{},
|
|
err: sql.ErrNoRows,
|
|
},
|
|
{
|
|
db: model.UserInfo{},
|
|
err: errors.New("invalid thing"),
|
|
},
|
|
}
|
|
|
|
for _, resp := range expectedResponses {
|
|
if resp.api == nil {
|
|
resp.api = &resp.db
|
|
}
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
mock.Ctx.Configuration.DuoAPI = &schema.DuoAPIConfiguration{}
|
|
|
|
// Set the initial user session.
|
|
userSession := mock.Ctx.GetSession()
|
|
userSession.Username = testUsername
|
|
userSession.AuthenticationLevel = 1
|
|
err := mock.Ctx.SaveSession(userSession)
|
|
require.NoError(t, err)
|
|
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
LoadUserInfo(mock.Ctx, gomock.Eq("john")).
|
|
Return(resp.db, resp.err)
|
|
|
|
UserInfoGET(mock.Ctx)
|
|
|
|
if resp.err == nil {
|
|
t.Run("expected status code", func(t *testing.T) {
|
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
|
})
|
|
|
|
actualPreferences := model.UserInfo{}
|
|
|
|
mock.GetResponseData(t, &actualPreferences)
|
|
|
|
t.Run("expected method", func(t *testing.T) {
|
|
assert.Equal(t, resp.api.Method, actualPreferences.Method)
|
|
})
|
|
|
|
t.Run("registered webauthn", func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
|
|
})
|
|
|
|
t.Run("registered totp", func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasTOTP, actualPreferences.HasTOTP)
|
|
})
|
|
|
|
t.Run("registered duo", func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasDuo, actualPreferences.HasDuo)
|
|
})
|
|
} else {
|
|
t.Run("expected status code", func(t *testing.T) {
|
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
|
})
|
|
|
|
errResponse := mock.GetResponseError(t)
|
|
|
|
assert.Equal(t, "KO", errResponse.Status)
|
|
assert.Equal(t, "Operation failed.", errResponse.Message)
|
|
}
|
|
|
|
mock.Close()
|
|
}
|
|
}
|
|
|
|
func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|
expectedResponses := []expectedResponseAlt{
|
|
{
|
|
description: "should set method to totp by default even when user doesn't have totp configured and no preferred method",
|
|
db: model.UserInfo{
|
|
Method: "",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: false,
|
|
},
|
|
api: &model.UserInfo{
|
|
Method: "totp",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: false,
|
|
},
|
|
config: &schema.Configuration{
|
|
DuoAPI: &schema.DuoAPIConfiguration{},
|
|
},
|
|
loadErr: nil,
|
|
saveErr: nil,
|
|
},
|
|
{
|
|
description: "should set method to duo by default when user has duo configured and no preferred method",
|
|
db: model.UserInfo{
|
|
Method: "",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: true,
|
|
},
|
|
api: &model.UserInfo{
|
|
Method: "mobile_push",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: true,
|
|
},
|
|
config: &schema.Configuration{
|
|
DuoAPI: &schema.DuoAPIConfiguration{},
|
|
},
|
|
loadErr: nil,
|
|
saveErr: nil,
|
|
},
|
|
{
|
|
description: "should set method to totp by default when user has duo configured and no preferred method but duo is not enabled",
|
|
db: model.UserInfo{
|
|
Method: "",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: true,
|
|
},
|
|
api: &model.UserInfo{
|
|
Method: "totp",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: true,
|
|
},
|
|
loadErr: nil,
|
|
saveErr: nil,
|
|
},
|
|
{
|
|
description: "should set method to duo by default when user has duo configured and no preferred method",
|
|
db: model.UserInfo{
|
|
Method: "",
|
|
HasTOTP: true,
|
|
HasWebauthn: true,
|
|
HasDuo: true,
|
|
},
|
|
api: &model.UserInfo{
|
|
Method: "webauthn",
|
|
HasTOTP: true,
|
|
HasWebauthn: true,
|
|
HasDuo: true,
|
|
},
|
|
config: &schema.Configuration{
|
|
TOTP: schema.TOTPConfiguration{
|
|
Disable: true,
|
|
},
|
|
DuoAPI: &schema.DuoAPIConfiguration{},
|
|
},
|
|
loadErr: nil,
|
|
saveErr: nil,
|
|
},
|
|
{
|
|
description: "should default new users to totp if all enabled",
|
|
db: model.UserInfo{
|
|
Method: "",
|
|
HasTOTP: false,
|
|
HasWebauthn: false,
|
|
HasDuo: false,
|
|
},
|
|
api: &model.UserInfo{
|
|
Method: "totp",
|
|
HasTOTP: true,
|
|
HasWebauthn: true,
|
|
HasDuo: true,
|
|
},
|
|
config: &schema.Configuration{
|
|
DuoAPI: &schema.DuoAPIConfiguration{},
|
|
},
|
|
loadErr: nil,
|
|
saveErr: errors.New("could not save"),
|
|
},
|
|
}
|
|
|
|
for _, resp := range expectedResponses {
|
|
if resp.api == nil {
|
|
resp.api = &resp.db
|
|
}
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
if resp.config != nil {
|
|
mock.Ctx.Configuration = *resp.config
|
|
}
|
|
|
|
// Set the initial user session.
|
|
userSession := mock.Ctx.GetSession()
|
|
userSession.Username = testUsername
|
|
userSession.AuthenticationLevel = 1
|
|
err := mock.Ctx.SaveSession(userSession)
|
|
require.NoError(t, err)
|
|
|
|
if resp.db.Method == "" {
|
|
gomock.InOrder(
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
LoadPreferred2FAMethod(mock.Ctx, gomock.Eq("john")).
|
|
Return("", sql.ErrNoRows),
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
SavePreferred2FAMethod(mock.Ctx, gomock.Eq("john"), gomock.Eq("")).
|
|
Return(resp.saveErr),
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
LoadUserInfo(mock.Ctx, gomock.Eq("john")).
|
|
Return(resp.db, nil),
|
|
mock.StorageMock.EXPECT().
|
|
SavePreferred2FAMethod(mock.Ctx, gomock.Eq("john"), gomock.Eq(resp.api.Method)).
|
|
Return(resp.saveErr),
|
|
)
|
|
} else {
|
|
gomock.InOrder(
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
LoadPreferred2FAMethod(mock.Ctx, gomock.Eq("john")).
|
|
Return(resp.db.Method, nil),
|
|
mock.StorageMock.
|
|
EXPECT().
|
|
LoadUserInfo(mock.Ctx, gomock.Eq("john")).
|
|
Return(resp.db, nil),
|
|
mock.StorageMock.EXPECT().
|
|
SavePreferred2FAMethod(mock.Ctx, gomock.Eq("john"), gomock.Eq(resp.api.Method)).
|
|
Return(resp.saveErr),
|
|
)
|
|
}
|
|
|
|
UserInfoPOST(mock.Ctx)
|
|
|
|
if resp.loadErr == nil && resp.saveErr == nil {
|
|
t.Run(fmt.Sprintf("%s/%s", resp.description, "expected status code"), func(t *testing.T) {
|
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
|
})
|
|
|
|
actualPreferences := model.UserInfo{}
|
|
|
|
mock.GetResponseData(t, &actualPreferences)
|
|
|
|
t.Run(fmt.Sprintf("%s/%s", resp.description, "expected method"), func(t *testing.T) {
|
|
assert.Equal(t, resp.api.Method, actualPreferences.Method)
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("%s/%s", resp.description, "registered webauthn"), func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("%s/%s", resp.description, "registered totp"), func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasTOTP, actualPreferences.HasTOTP)
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("%s/%s", resp.description, "registered duo"), func(t *testing.T) {
|
|
assert.Equal(t, resp.api.HasDuo, actualPreferences.HasDuo)
|
|
})
|
|
} else {
|
|
t.Run("expected status code", func(t *testing.T) {
|
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
|
})
|
|
|
|
errResponse := mock.GetResponseError(t)
|
|
|
|
assert.Equal(t, "KO", errResponse.Status)
|
|
assert.Equal(t, "Operation failed.", errResponse.Message)
|
|
}
|
|
|
|
mock.Close()
|
|
}
|
|
}
|
|
|
|
func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() {
|
|
s.mock.StorageMock.EXPECT().
|
|
LoadUserInfo(s.mock.Ctx, gomock.Eq("john")).
|
|
Return(model.UserInfo{}, fmt.Errorf("failure"))
|
|
|
|
UserInfoGET(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unable to load user information: failure", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func TestFetchSuite(t *testing.T) {
|
|
suite.Run(t, &FetchSuite{})
|
|
}
|
|
|
|
type SaveSuite struct {
|
|
suite.Suite
|
|
mock *mocks.MockAutheliaCtx
|
|
}
|
|
|
|
func (s *SaveSuite) SetupTest() {
|
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
|
// Set the initial user session.
|
|
userSession := s.mock.Ctx.GetSession()
|
|
userSession.Username = testUsername
|
|
userSession.AuthenticationLevel = 1
|
|
err := s.mock.Ctx.SaveSession(userSession)
|
|
require.NoError(s.T(), err)
|
|
}
|
|
|
|
func (s *SaveSuite) TearDownTest() {
|
|
s.mock.Close()
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturnError500WhenNoBodyProvided() {
|
|
s.mock.Ctx.Request.SetBody(nil)
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturnError500WhenMalformedBodyProvided() {
|
|
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\""))
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturnError500WhenBadBodyProvided() {
|
|
s.mock.Ctx.Request.SetBody([]byte("{\"weird_key\":\"abc\"}"))
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unable to validate body: method: non zero value required", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() {
|
|
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"}"))
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unknown or unavailable method 'abc', it should be one of totp, webauthn", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
|
|
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
|
|
s.mock.StorageMock.EXPECT().
|
|
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("webauthn")).
|
|
Return(fmt.Errorf("Failure"))
|
|
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
s.mock.Assert200KO(s.T(), "Operation failed.")
|
|
assert.Equal(s.T(), "unable to save new preferred 2FA method: Failure", s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
|
|
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
|
|
s.mock.StorageMock.EXPECT().
|
|
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("webauthn")).
|
|
Return(nil)
|
|
|
|
MethodPreferencePost(s.mock.Ctx)
|
|
|
|
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
|
}
|
|
|
|
func TestSaveSuite(t *testing.T) {
|
|
suite.Run(t, &SaveSuite{})
|
|
}
|