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{}) }