fix: close expired test attempts at the attempt limit
This commit is contained in:
@@ -286,6 +286,9 @@ func (s *Service) Start(ctx context.Context, userID uint, slug, category string)
|
|||||||
|
|
||||||
func (s *Service) startTest(ctx context.Context, userID uint, quiz *models.Quiz) (*Started, error) {
|
func (s *Service) startTest(ctx context.Context, userID uint, quiz *models.Quiz) (*Started, error) {
|
||||||
var st *Started
|
var st *Started
|
||||||
|
// limited refuses the start after the commit, so the expired attempts
|
||||||
|
// closed on the way stay closed (an error would roll the closes back).
|
||||||
|
var limited bool
|
||||||
err := lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
err := lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||||
tx = tx.WithContext(ctx)
|
tx = tx.WithContext(ctx)
|
||||||
if err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext(?))`, AttemptLockKey(quiz.ID, userID)).Error; err != nil {
|
if err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext(?))`, AttemptLockKey(quiz.ID, userID)).Error; err != nil {
|
||||||
@@ -327,7 +330,8 @@ func (s *Service) startTest(ctx context.Context, userID uint, quiz *models.Quiz)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if left := AttemptsLeft(quiz, int(used)); left != nil && *left == 0 {
|
if left := AttemptsLeft(quiz, int(used)); left != nil && *left == 0 {
|
||||||
return ErrAttemptLimit
|
limited = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
attempt := &models.Attempt{QuizID: quiz.ID, UserID: userID, StartedAt: now.UTC()}
|
attempt := &models.Attempt{QuizID: quiz.ID, UserID: userID, StartedAt: now.UTC()}
|
||||||
maxPoints, err := s.maxPoints(ctx, tx, attempt)
|
maxPoints, err := s.maxPoints(ctx, tx, attempt)
|
||||||
@@ -344,6 +348,9 @@ func (s *Service) startTest(ctx context.Context, userID uint, quiz *models.Quiz)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if limited {
|
||||||
|
return nil, ErrAttemptLimit
|
||||||
|
}
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,38 @@ VALUES ('Stary', 'stary', true, 50, false, NOW(), NOW())`).Error)
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("the last expired attempt is closed at the limit", func(t *testing.T) {
|
||||||
|
f := seedQuiz(t, gdb, "test-wygasly-limit", test(func(q *models.Quiz) {
|
||||||
|
q.TimeLimitSeconds = intPtr(60)
|
||||||
|
q.MaxAttempts = intPtr(1)
|
||||||
|
}))
|
||||||
|
u, _ := newUser(t, gdb, "lazy-limit@example.com")
|
||||||
|
t0 := time.Now().UTC().Truncate(time.Second)
|
||||||
|
clock := t0
|
||||||
|
svc := &classes.Service{DB: gdb, Now: func() time.Time { return clock }, Grace: 5 * time.Second}
|
||||||
|
st, err := svc.Start(t.Context(), u.ID, "test-wygasly-limit", "")
|
||||||
|
must(t, err)
|
||||||
|
clock = t0.Add(10 * time.Second)
|
||||||
|
_, err = svc.Answer(t.Context(), u.ID, st.Attempt.ID, classes.AnswerInput{QuestionID: f.single.ID, AnswerIDs: []uint{f.singleRight.ID}})
|
||||||
|
must(t, err)
|
||||||
|
|
||||||
|
clock = t0.Add(70 * time.Second)
|
||||||
|
if _, err := svc.Start(t.Context(), u.ID, "test-wygasly-limit", ""); !errors.Is(err, classes.ErrAttemptLimit) {
|
||||||
|
t.Errorf("start after the last attempt expired = %v, want ErrAttemptLimit", err)
|
||||||
|
}
|
||||||
|
var old models.Attempt
|
||||||
|
must(t, gdb.Take(&old, st.Attempt.ID).Error)
|
||||||
|
if old.CompletedAt == nil || !old.CompletedAt.Equal(t0.Add(60*time.Second)) {
|
||||||
|
t.Errorf("expired attempt completed_at = %v, want %v (closed despite the limit)", old.CompletedAt, t0.Add(60*time.Second))
|
||||||
|
}
|
||||||
|
if old.ScorePoints != 1 || old.MaxPoints != 4 {
|
||||||
|
t.Errorf("expired attempt score = %v / %d, want 1 / 4", old.ScorePoints, old.MaxPoints)
|
||||||
|
}
|
||||||
|
if n := countAttempts(t, f.quiz.ID, u.ID); n != 1 {
|
||||||
|
t.Errorf("attempts after the refused start = %d, want 1", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("a shuffled test keeps its order", func(t *testing.T) {
|
t.Run("a shuffled test keeps its order", func(t *testing.T) {
|
||||||
c := &client{t: t, h: h}
|
c := &client{t: t, h: h}
|
||||||
seedQuiz(t, gdb, "test-losowy", test(func(q *models.Quiz) { q.ShuffleQuestions = true }))
|
seedQuiz(t, gdb, "test-losowy", test(func(q *models.Quiz) { q.ShuffleQuestions = true }))
|
||||||
|
|||||||
Reference in New Issue
Block a user