test(webtests): add end-to-end TOTP lockout test

Drives the login endpoint through 11 failed TOTP attempts against user10
and asserts the account ends up locked in the database, then verifies a
subsequent login with a valid TOTP code is rejected with
ErrCodeAccountLocked. Exercises the GHSA-fgfv-pv97-6cmj regression
against the real handler path.
This commit is contained in:
kolaente 2026-04-09 17:16:25 +02:00 committed by kolaente
parent 75629158cb
commit 6ca0151d02
1 changed files with 47 additions and 0 deletions

View File

@ -19,10 +19,13 @@ package webtests
import (
"net/http"
"testing"
"time"
"code.vikunja.io/api/pkg/db"
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/api/pkg/user"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -66,3 +69,47 @@ func TestLogin(t *testing.T) {
assertHandlerErrorCode(t, err, user.ErrCodeEmailNotConfirmed)
})
}
func TestLoginTOTPLockout(t *testing.T) {
// user10 fixture: TOTP secret JBSWY3DPEHPK3PXP, password 12345678.
const totpSecret = "JBSWY3DPEHPK3PXP" //nolint:gosec
// Share one env across requests: setupTestEnv re-inits keyvalue on each
// call, so using newTestRequest would reset the attempt counter every
// iteration and the lockout would never trigger.
e, err := setupTestEnv()
require.NoError(t, err)
invalidPayload := `{
"username": "user10",
"password": "12345678",
"totp_passcode": "000000"
}`
for i := 0; i < 11; i++ {
c, _ := createRequest(e, http.MethodPost, invalidPayload, nil, nil)
err := apiv1.Login(c)
require.Error(t, err)
}
s := db.NewSession()
locked := &user.User{}
exists, err := s.Where("id = ?", 10).Get(locked)
require.NoError(t, err)
require.True(t, exists)
require.NoError(t, s.Close())
assert.Equal(t, user.StatusAccountLocked, locked.Status,
"user10 should be locked after 10 failed TOTP attempts")
validCode, err := totp.GenerateCode(totpSecret, time.Now())
require.NoError(t, err)
validPayload := `{
"username": "user10",
"password": "12345678",
"totp_passcode": "` + validCode + `"
}`
c, _ := createRequest(e, http.MethodPost, validPayload, nil, nil)
err = apiv1.Login(c)
require.Error(t, err)
assertHandlerErrorCode(t, err, user.ErrCodeAccountLocked)
}