Skip to content

Commit

Permalink
kyc: quiz: add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-dionysos committed Nov 27, 2023
1 parent d482ef0 commit eca1509
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 49 deletions.
2 changes: 1 addition & 1 deletion cmd/eskimo-hut/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ type (
StartOrContinueKYCStep4SessionRequestBody struct {
SelectedOption *uint8 `form:"selectedOption" required:"true" swaggerignore:"true" example:"0"`
Language string `form:"language" required:"true" swaggerignore:"true" example:"en"`
QuestionNumber uint8 `form:"questionNumber" required:"true" swaggerignore:"true" example:"11"`
QuestionNumber uint `form:"questionNumber" required:"true" swaggerignore:"true" example:"11"`
}
VerifySocialKYCStepRequestBody struct {
Social kycsocial.Type `form:"social" required:"true" swaggerignore:"true" example:"twitter"`
Expand Down
2 changes: 1 addition & 1 deletion cmd/eskimo-hut/kyc.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen,reviv
fmt.Sprintf("[%v]Ice is not mined, but it turns out immediately after registration", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
fmt.Sprintf("[%v]Ice is cool", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd // .
},
Number: uint8(11 + rand.Intn(20)), //nolint:gosec,gomnd // .
Number: uint(11 + rand.Intn(20)), //nolint:gosec,gomnd // .
Text: fmt.Sprintf("[%v][%v] What are the major differences between Ice, Pi and Bee?", req.Data.Language, strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
},
MaxQuestions: uint8(30 + rand.Intn(40)), //nolint:gosec,gomnd // .
Expand Down
18 changes: 13 additions & 5 deletions kyc/quiz/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ const (
)

type (
UserID = users.UserID
UserID = users.UserID
UserProfile = users.UserProfile

Manager interface {
io.Closer

Start(ctx context.Context, userID UserID, lang string, total int) (Quiz, error)
Continue(ctx context.Context, userID UserID, question, answer uint8) (Quiz, error)
Continue(ctx context.Context, userID UserID, question uint, answer uint8) (Quiz, error)
}

UserReader interface {
GetUserByID(ctx context.Context, userID UserID) (*UserProfile, error)
}

Result string
Expand All @@ -46,7 +51,7 @@ type (
Question struct {
Text string `json:"text" example:"Какая температура на улице?" db:"question"`
Options []string `json:"options" example:"+21,-2,+33,0" db:"options"`
Number uint8 `json:"number" example:"1" db:"id"`
Number uint `json:"number" example:"1" db:"id"`
}
)

Expand All @@ -59,6 +64,7 @@ var (
ErrSessionFinished = errors.New("session closed")
ErrSessionExpired = errors.New("session expired")
ErrUnknownQuestionNumber = errors.New("unknown question number")
ErrUnknownSession = errors.New("unknown session and/or user")
)

const (
Expand All @@ -68,9 +74,11 @@ const (
defaultQuestionsCacheExpireSeconds = 60 * 60 * 24 // 1 day.
)

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

_ UserReader = users.ReadRepository(nil)
)

type (
Expand All @@ -82,7 +90,7 @@ type (
managerImpl struct {
DB *storage.DB
Shutdown func() error
Users users.Repository
Users UserReader
Cache struct {
Questions map[string]quizDetails
Timestamp *time.Time
Expand Down
68 changes: 33 additions & 35 deletions kyc/quiz/quiz.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strconv"
stdlibtime "time"

"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/valyala/fastrand"

Expand All @@ -34,13 +33,16 @@ func loadConfig() config {
return cfg
}

func NewManager(ctx context.Context) Manager {
repo := users.New(ctx, nil)
func NewManager(ctx context.Context, userReader UserReader) Manager {
return newManagerImpl(ctx, userReader)
}

func newManagerImpl(ctx context.Context, userReader UserReader) *managerImpl {
db := storage.MustConnect(ctx, ddl, applicationYamlKey)
manager := &managerImpl{
DB: db,
Shutdown: db.Close,
Users: repo,
Users: userReader,
config: loadConfig(),
}
manager.Cache.Timestamp = time.New(stdlibtime.Time{})
Expand Down Expand Up @@ -136,15 +138,11 @@ func (m *managerImpl) Close() (err error) {
err = m.Shutdown()
}

if m.Users != nil {
err = multierror.Append(err, m.Users.Close()).ErrorOrNil()
}

return
}

func questionsToSlice(questions []Question) []uint8 {
s := make([]uint8, 0, len(questions))
func questionsToSlice(questions []Question) []uint {
s := make([]uint, 0, len(questions))
for i := range questions {
s = append(s, questions[i].Number)
}
Expand Down Expand Up @@ -258,7 +256,7 @@ func (m *managerImpl) Start(ctx context.Context, userID UserID, lang string, tot
return Quiz{
Progress: &Progress{
ExpiresAt: time.New(expires),
MaxQuestions: uint8(len(questions)),
MaxQuestions: uint8(len(questions)) - 1,
NextQuestion: &Question{
Text: questions[0].Text,
Options: questions[0].Options,
Expand All @@ -277,13 +275,15 @@ func calculateProgress(correctAnswers, currentAnswers []int) (correctNum, incorr
for i := range correct {
if correct[i] == currentAnswers[i] {
correctNum++
} else {
incorrectNum++
}
}

return correctNum, uint8(len(currentAnswers) - len(correct))
return
}

func (m *managerImpl) Continue(ctx context.Context, userID UserID, question, answer uint8) (Quiz, error) { //nolint:funlen // It's expected.
func (m *managerImpl) Continue(ctx context.Context, userID UserID, question uint, answer uint8) (Quiz, error) { //nolint:funlen // It's expected.
// $1: user_id
// $2: question
// $3: answer
Expand All @@ -309,15 +309,15 @@ func (m *managerImpl) Continue(ctx context.Context, userID UserID, question, ans
),
corrent_answers as (
select
array_agg(questions.correct_option) as ids
array_agg(questions.correct_option order by q.nr) as ids
from
questions,
session_data
session_data,
questions
inner join unnest(session_data.questions) with ordinality AS q(id, nr)
on questions.id = q.id
where
session_data.valid is true and
questions.language = session_data.language and
questions.quiz_id = session_data.quiz_id and
questions.id = ANY(ARRAY[session_data.questions])
questions.quiz_id = session_data.quiz_id
),
session_update as (
update quizz_sessions
Expand Down Expand Up @@ -356,12 +356,11 @@ func (m *managerImpl) Continue(ctx context.Context, userID UserID, question, ans
questions.id = session_data.questions[session_data.question_idx + 1]
)
select
session_data.user_id,
COALESCE(session_data.expired, false) as expired,
COALESCE(session_data.expires_at, now()) as expires_at,
COALESCE(session_data.finished, false) as finished,
COALESCE(session_data.valid, false) as valid,
COALESCE(array_length(session_data.questions, 1) - session_data.question_idx + 1, 0) as left,
session_data.expired,
session_data.expires_at,
session_data.finished,
session_data.valid,
COALESCE((array_length(session_data.questions, 1) - session_data.question_idx) - 1, 0) as left,
COALESCE(next_question.id, 0) as id,
COALESCE(next_question.question, '') as question,
Expand All @@ -374,12 +373,11 @@ func (m *managerImpl) Continue(ctx context.Context, userID UserID, question, ans
COALESCE(session_update.answers, session_data.answers, '{}'::INT[]) as current_answers
from
session_data
full outer join next_question on true
full outer join session_update on true
full outer join corrent_answers on true
left join next_question on true
left join session_update on true
left join corrent_answers on true
`
type PipelineResult struct { //nolint:govet // Let's keep it as is for better readability.
User *string `db:"user_id"`
type PipelineResult struct { //nolint:govet // Let's keep it as is for better readability.`
Expired bool `db:"expired"`
ExpiresAt time.Time `db:"expires_at"`
Finished bool `db:"finished"`
Expand All @@ -396,20 +394,20 @@ func (m *managerImpl) Continue(ctx context.Context, userID UserID, question, ans

result, err := storage.Get[PipelineResult](ctx, m.DB, pipeline, userID, int(question), int(answer), m.config.MaxSessionDurationSeconds)
if err != nil {
if errors.As(err, &storage.ErrNotFound) {
return Quiz{}, ErrUnknownUser
if errors.Is(err, storage.ErrNotFound) {
return Quiz{}, ErrUnknownSession
}

return Quiz{}, errors.Wrap(err, "failed to continue session")
}

switch {
case result.User == nil:
return Quiz{}, ErrUnknownUser

case result.Expired && !result.Finished:
return Quiz{}, ErrSessionExpired

case result.Valid && result.Finished && !result.Ended:
return Quiz{}, ErrSessionFinished

case !result.Valid:
return Quiz{}, ErrUnknownQuestionNumber

Expand Down
Loading

0 comments on commit eca1509

Please sign in to comment.