Skip to content

Commit

Permalink
Fix kyc quiz expired (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-dionysos authored Jan 15, 2024
1 parent 286f963 commit cd4960f
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 38 deletions.
4 changes: 2 additions & 2 deletions cmd/eskimo-hut/kyc.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen // .
case errors.Is(err, kycquiz.ErrUnknownUser) || errors.Is(err, kycquiz.ErrUnknownSession):
return nil, server.NotFound(err, userNotFoundErrorCode)

case errors.Is(err, kycquiz.ErrSessionExpired), errors.Is(err, kycquiz.ErrSessionFinished), errors.Is(err, kycquiz.ErrSessionFinishedWithError), errors.Is(err, kycquiz.ErrInvalidKYCState): //nolint:lll // .
case errors.Is(err, kycquiz.ErrSessionFinished), errors.Is(err, kycquiz.ErrSessionFinishedWithError), errors.Is(err, kycquiz.ErrInvalidKYCState): //nolint:lll // .
return nil, server.BadRequest(err, raceConditionErrorCode)

case errors.Is(err, kycquiz.ErrUnknownQuestionNumber):
Expand Down Expand Up @@ -244,7 +244,7 @@ func (s *service) TryResetKYCSteps( //nolint:gocritic,funlen,gocognit,revive,cyc
}
case users.QuizKYCStep:
if err := s.quizRepository.SkipQuizSession(ctx, req.Data.UserID); err != nil {
if errors.Is(err, kycquiz.ErrInvalidKYCState) || errors.Is(err, kycquiz.ErrSessionFinished) || errors.Is(err, kycquiz.ErrSessionFinishedWithError) || errors.Is(err, kycquiz.ErrSessionExpired) { //nolint:lll // .
if errors.Is(err, kycquiz.ErrInvalidKYCState) || errors.Is(err, kycquiz.ErrSessionFinished) || errors.Is(err, kycquiz.ErrSessionFinishedWithError) { //nolint:lll // .
log.Error(errors.Wrapf(err, "skipQuizSession failed unexpectedly during tryResetKYCSteps for userID:%v", req.Data.UserID))
err = nil
}
Expand Down
5 changes: 3 additions & 2 deletions kyc/quiz/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ var (
ErrSessionIsAlreadyRunning = newError("another session is already running")
ErrSessionFinished = newError("session closed")
ErrSessionFinishedWithError = newError("session closed with error")
ErrSessionExpired = newError("session expired")
ErrUnknownQuestionNumber = newError("unknown question number")
ErrUnknownSession = newError("unknown session and/or user")
)
Expand All @@ -78,9 +77,11 @@ const (
applicationYamlKey = "kyc/quiz"
)

var ( //nolint:gofumpt //.
var (
//go:embed DDL.sql
ddl string

errSessionExpired = newError("session expired")
)

type (
Expand Down
117 changes: 93 additions & 24 deletions kyc/quiz/quiz.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) err
// $1: user_id.
const stmt = `
select
started_at,
ended_at is not null as finished,
ended_successfully
from
Expand All @@ -119,9 +118,8 @@ func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) err
now := time.Now()

data, err := storage.ExecOne[struct {
StartedAt *time.Time `db:"started_at"`
Finished bool `db:"finished"`
Success bool `db:"ended_successfully"`
Finished bool `db:"finished"`
Success bool `db:"ended_successfully"`
}](ctx, tx, stmt, userID)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
Expand All @@ -131,14 +129,7 @@ func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) err
return errors.Wrap(wrapErrorInTx(err), "failed to get session data")
}

switch {
case data.StartedAt == nil:
return wrapErrorInTx(ErrUnknownSession)

case data.StartedAt.Add(stdlibtime.Duration(r.config.MaxSessionDurationSeconds) * stdlibtime.Second).Before(*now.Time):
return wrapErrorInTx(ErrSessionExpired)

case data.Finished:
if data.Finished {
if data.Success {
return wrapErrorInTx(ErrSessionFinished)
}
Expand Down Expand Up @@ -199,17 +190,63 @@ func wrapErrorInTx(err error) error {
return err
}

func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, lang string) (*Quiz, error) { //nolint:funlen //.
err := r.CheckUserKYC(ctx, userID)
func (r *repositoryImpl) finishExpiredSession( //nolint:funlen //.
ctx context.Context,
userID UserID,
now *time.Time,
tx storage.QueryExecer,
) (*time.Time, error) {
// $1: user_id.
// $2: max session duration (seconds).
// $3: session cool down (seconds).
const stmt = `
with result as (
update quiz_sessions
set
ended_at = now(),
ended_successfully = false
where
user_id = $1 and
ended_at is null and
started_at + make_interval(secs => $2) < now()
returning *
)
insert into failed_quiz_sessions (started_at, ended_at, questions, answers, language, user_id, skipped)
select
result.started_at,
result.ended_at,
result.questions,
result.answers,
result.language,
result.user_id,
false
from
result
returning
ended_at + make_interval(secs => $3) as cooldown_at
`
data, err := storage.ExecOne[struct {
CooldownAt *time.Time `db:"cooldown_at"`
}](ctx, tx, stmt, userID, r.config.MaxSessionDurationSeconds, r.config.SessionCoolDownSeconds)
if err != nil {
return nil, err
}
if errors.Is(err, storage.ErrNotFound) {
err = nil
}

questions, err := r.SelectQuestions(ctx, r.DB, lang)
if err != nil {
return nil, err
}

return data.CooldownAt, errors.Wrapf(r.modifyUser(ctx, false, now, userID), "failed to modifyUser")
}

func (r *repositoryImpl) startNewSession( //nolint:funlen,revive //.
ctx context.Context,
userID UserID,
now *time.Time,
tx storage.QueryExecer,
lang string,
questions []*Question,
) (*Quiz, error) {
// $1: user_id.
// $2: language.
// $3: questions.
Expand Down Expand Up @@ -294,7 +331,7 @@ func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, la
ActiveEndedAt *time.Time `db:"active_ended_at"`
UpsertStartedAt *time.Time `db:"upsert_started_at"`
UpsertDeadline *time.Time `db:"upsert_deadline"`
}](ctx, r.DB, stmt, userID, lang, questionsToSlice(questions), r.config.SessionCoolDownSeconds, r.config.MaxSessionDurationSeconds)
}](ctx, tx, stmt, userID, lang, questionsToSlice(questions), r.config.SessionCoolDownSeconds, r.config.MaxSessionDurationSeconds)
if err != nil {
if errors.Is(err, storage.ErrRelationNotFound) {
err = ErrUnknownUser
Expand All @@ -303,7 +340,6 @@ func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, la
return nil, errors.Wrap(err, "failed to start session")
}

now := stdlibtime.Now().Truncate(stdlibtime.Second).UTC()
switch {
case data.FailedAt != nil: // Failed session is still in cool down.
return nil, errors.Wrapf(ErrSessionFinishedWithError, "wait until %v",
Expand All @@ -318,8 +354,8 @@ func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, la
return nil, ErrSessionFinishedWithError
}

if data.ActiveDeadline.After(now) {
return nil, errors.Wrapf(ErrSessionIsAlreadyRunning, "wait %s before next session", data.ActiveDeadline.Sub(now))
if data.ActiveDeadline.After(*now.Time) {
return nil, errors.Wrapf(ErrSessionIsAlreadyRunning, "wait %s before next session", data.ActiveDeadline.Sub(*now.Time))
}

case data.UpsertStartedAt != nil: // New session is started.
Expand All @@ -335,6 +371,34 @@ func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, la
panic("unreachable: " + userID)
}

func (r *repositoryImpl) StartQuizSession(ctx context.Context, userID UserID, lang string) (quiz *Quiz, err error) {
questions, err := r.SelectQuestions(ctx, r.DB, lang)
if err != nil {
return nil, err
}

err = storage.DoInTransaction(ctx, r.DB, func(tx storage.QueryExecer) error {
now := time.Now()
cooldown, fErr := r.finishExpiredSession(ctx, userID, now, tx)
if fErr != nil {
return wrapErrorInTx(fErr)
} else if cooldown != nil {
return wrapErrorInTx(errors.Wrapf(ErrSessionFinishedWithError, "wait until %v", cooldown))
}

err = r.CheckUserKYC(ctx, userID)
if err != nil {
return wrapErrorInTx(err)
}

quiz, err = r.startNewSession(ctx, userID, now, tx, lang, questions)

return wrapErrorInTx(err)
})

return quiz, err
}

func calculateProgress(correctAnswers, currentAnswers []uint8) (correctNum, incorrectNum uint8) {
correct := correctAnswers
if len(currentAnswers) < len(correctAnswers) {
Expand Down Expand Up @@ -412,7 +476,7 @@ group by

deadline := data.StartedAt.Add(stdlibtime.Duration(r.config.MaxSessionDurationSeconds) * stdlibtime.Second)
if deadline.Before(now) {
return userProgress{}, ErrSessionExpired
return userProgress{}, errSessionExpired
}

return data.userProgress, nil
Expand Down Expand Up @@ -555,7 +619,7 @@ func (r *repositoryImpl) modifyUser(ctx context.Context, success bool, now *time
return errors.Wrapf(r.Users.ModifyUser(ctx, usr, nil), "failed to modify user %#v", usr)
}

func (r *repositoryImpl) ContinueQuizSession( //nolint:funlen,revive //.
func (r *repositoryImpl) ContinueQuizSession( //nolint:funlen,revive,gocognit //.
ctx context.Context,
userID UserID,
question, answer uint8,
Expand All @@ -564,6 +628,11 @@ func (r *repositoryImpl) ContinueQuizSession( //nolint:funlen,revive //.
now := stdlibtime.Now().Truncate(stdlibtime.Second).UTC()
progress, pErr := r.CheckUserRunningSession(ctx, userID, now, tx)
if pErr != nil {
if errors.Is(pErr, errSessionExpired) {
quiz = &Quiz{Result: FailureResult}
pErr = r.UserMarkSessionAsFinished(ctx, userID, now, tx, false, false)
}

return wrapErrorInTx(pErr)
}
_, err = r.CheckQuestionNumber(ctx, progress.Lang, progress.Questions, question, tx)
Expand Down
20 changes: 10 additions & 10 deletions kyc/quiz/quiz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,8 @@ func testManagerSessionStart(ctx context.Context, t *testing.T, r *repositoryImp
t.Run("Expired", func(t *testing.T) {
helperForceResetSessionStartedAt(t, r, "bogus")
session, err := r.StartQuizSession(ctx, "bogus", "en")
require.NoError(t, err)
require.NotNil(t, session)
require.NotNil(t, session.Progress)
require.NotNil(t, session.Progress.ExpiresAt)
require.NotEmpty(t, session.Progress.NextQuestion)
require.Equal(t, uint8(3), session.Progress.MaxQuestions)
require.Equal(t, uint8(1), session.Progress.NextQuestion.Number)
require.ErrorIs(t, err, ErrSessionFinishedWithError)
require.Nil(t, session)
})
t.Run("CoolDown", func(t *testing.T) {
helperSessionReset(t, r, "bogus", true)
Expand Down Expand Up @@ -218,7 +213,7 @@ func testManagerSessionSkip(ctx context.Context, t *testing.T, r *repositoryImpl
helperForceResetSessionStartedAt(t, r, "bogus")

err = r.SkipQuizSession(ctx, "bogus")
require.ErrorIs(t, err, ErrSessionExpired)
require.NoError(t, err)
})
t.Run("Finished", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
Expand Down Expand Up @@ -280,8 +275,13 @@ func testManagerSessionContinueErrors(ctx context.Context, t *testing.T, r *repo
data, err := r.StartQuizSession(ctx, "bogus", "en")
require.NoError(t, err)
helperForceResetSessionStartedAt(t, r, "bogus")
_, err = r.ContinueQuizSession(ctx, "bogus", data.Progress.NextQuestion.Number, 1)
require.ErrorIs(t, err, ErrSessionExpired)
data, err = r.ContinueQuizSession(ctx, "bogus", data.Progress.NextQuestion.Number, 1)
require.NoError(t, err)
require.Nil(t, data.Progress)
require.Equal(t, FailureResult, data.Result)

_, err = r.StartQuizSession(ctx, "bogus", "en")
require.ErrorIs(t, err, ErrSessionFinishedWithError)
})

t.Run("UnknownQuestionNumber", func(t *testing.T) {
Expand Down

0 comments on commit cd4960f

Please sign in to comment.