Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix kyc quiz expired #96

Merged
merged 3 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 94 additions & 13 deletions kyc/quiz/quiz.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@ func (r *repositoryImpl) validateKycStep(user *users.User) error {

func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) error { //nolint:funlen //.
// $1: user_id.
// $2: max session duration (seconds).
const stmt = `
select
started_at,
started_at + make_interval(secs => $2) as deadline,
ended_at is not null as finished,
ended_successfully
from
Expand All @@ -120,9 +122,10 @@ func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) err

data, err := storage.ExecOne[struct {
StartedAt *time.Time `db:"started_at"`
Deadline *time.Time `db:"deadline"`
Finished bool `db:"finished"`
Success bool `db:"ended_successfully"`
}](ctx, tx, stmt, userID)
}](ctx, tx, stmt, userID, r.config.MaxSessionDurationSeconds)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return wrapErrorInTx(ErrUnknownSession)
Expand All @@ -135,7 +138,7 @@ func (r *repositoryImpl) SkipQuizSession(ctx context.Context, userID UserID) err
case data.StartedAt == nil:
return wrapErrorInTx(ErrUnknownSession)

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

case data.Finished:
Expand Down Expand Up @@ -199,17 +202,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 +343,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 +352,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 +366,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 +383,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) {
err = r.CheckUserKYC(ctx, userID)
if err != nil {
return nil, err
}

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

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 @@ -555,7 +631,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 +640,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
18 changes: 9 additions & 9 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 @@ -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
Loading