You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package question
|
|
|
|
// Tests for question application service behavior using fake repository and cache adapters.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
domain "knowfoolery/backend/services/question-bank-service/internal/domain/question"
|
|
)
|
|
|
|
// fakeRepo is an in-memory repository double for service tests.
|
|
type fakeRepo struct {
|
|
items []*domain.Question
|
|
}
|
|
|
|
// GetByID returns a fake question by ID.
|
|
func (f *fakeRepo) GetByID(ctx context.Context, id string) (*domain.Question, error) {
|
|
for _, q := range f.items {
|
|
if q.ID == id {
|
|
return q, nil
|
|
}
|
|
}
|
|
return nil, domain.ErrQuestionNotFound
|
|
}
|
|
|
|
// Create appends a fake question and assigns a deterministic ID.
|
|
func (f *fakeRepo) Create(ctx context.Context, q *domain.Question) (*domain.Question, error) {
|
|
q.ID = "id-1"
|
|
f.items = append(f.items, q)
|
|
return q, nil
|
|
}
|
|
|
|
// Update returns the provided fake question as updated.
|
|
func (f *fakeRepo) Update(ctx context.Context, id string, q *domain.Question) (*domain.Question, error) {
|
|
return q, nil
|
|
}
|
|
|
|
// SoftDelete is a no-op fake delete implementation.
|
|
func (f *fakeRepo) SoftDelete(ctx context.Context, id string) error { return nil }
|
|
|
|
// ListThemes returns a fixed fake theme set.
|
|
func (f *fakeRepo) ListThemes(ctx context.Context) ([]string, error) {
|
|
return []string{"Science"}, nil
|
|
}
|
|
|
|
// CountRandomCandidates returns fake candidate count from in-memory state.
|
|
func (f *fakeRepo) CountRandomCandidates(ctx context.Context, filter domain.RandomFilter) (int, error) {
|
|
if len(f.items) == 0 {
|
|
return 0, nil
|
|
}
|
|
return len(f.items), nil
|
|
}
|
|
|
|
// RandomByOffset returns a fake candidate by offset.
|
|
func (f *fakeRepo) RandomByOffset(
|
|
ctx context.Context,
|
|
filter domain.RandomFilter,
|
|
offset int,
|
|
) (*domain.Question, error) {
|
|
if len(f.items) == 0 {
|
|
return nil, domain.ErrNoQuestionsAvailable
|
|
}
|
|
if offset >= len(f.items) {
|
|
offset = 0
|
|
}
|
|
return f.items[offset], nil
|
|
}
|
|
|
|
// BulkCreate appends all incoming questions and reports all as created.
|
|
func (f *fakeRepo) BulkCreate(
|
|
ctx context.Context,
|
|
questions []*domain.Question,
|
|
) (int, []domain.BulkError, error) {
|
|
f.items = append(f.items, questions...)
|
|
return len(questions), nil, nil
|
|
}
|
|
|
|
// fakeCache is a simple in-memory cache double.
|
|
type fakeCache struct {
|
|
store map[string]*domain.Question
|
|
}
|
|
|
|
// Get returns a cached fake question when present.
|
|
func (f *fakeCache) Get(ctx context.Context, key string) (*domain.Question, bool) {
|
|
q, ok := f.store[key]
|
|
return q, ok
|
|
}
|
|
|
|
// Set stores a fake question in cache.
|
|
func (f *fakeCache) Set(ctx context.Context, key string, q *domain.Question, ttl time.Duration) {
|
|
if f.store == nil {
|
|
f.store = map[string]*domain.Question{}
|
|
}
|
|
f.store[key] = q
|
|
}
|
|
|
|
// Invalidate is a no-op for tests.
|
|
func (f *fakeCache) Invalidate(ctx context.Context) {}
|
|
|
|
// TestGetRandomQuestion_NoQuestions returns ErrNoQuestionsAvailable when no active candidates exist.
|
|
func TestGetRandomQuestion_NoQuestions(t *testing.T) {
|
|
svc := NewService(&fakeRepo{}, &fakeCache{}, time.Minute, 200)
|
|
_, err := svc.GetRandomQuestion(context.Background(), RandomQuestionRequest{})
|
|
if !errors.Is(err, domain.ErrNoQuestionsAvailable) {
|
|
t.Fatalf("expected no questions error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestGetRandomQuestion_WithCache returns a random question successfully with cache enabled.
|
|
func TestGetRandomQuestion_WithCache(t *testing.T) {
|
|
repo := &fakeRepo{items: []*domain.Question{
|
|
{
|
|
ID: "q1",
|
|
Theme: "Science",
|
|
Text: "Q",
|
|
Answer: "A",
|
|
Difficulty: domain.DifficultyMedium,
|
|
IsActive: true,
|
|
},
|
|
}}
|
|
cache := &fakeCache{store: map[string]*domain.Question{}}
|
|
svc := NewService(repo, cache, time.Minute, 200)
|
|
|
|
q, err := svc.GetRandomQuestion(context.Background(), RandomQuestionRequest{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q.ID == "" {
|
|
t.Fatal("expected id")
|
|
}
|
|
}
|
|
|
|
// TestValidateAnswerByQuestionID returns a matched result and expected threshold for equivalent answers.
|
|
func TestValidateAnswerByQuestionID(t *testing.T) {
|
|
repo := &fakeRepo{items: []*domain.Question{
|
|
{
|
|
ID: "q1",
|
|
Theme: "Science",
|
|
Text: "Q",
|
|
Answer: "paris",
|
|
Difficulty: domain.DifficultyEasy,
|
|
IsActive: true,
|
|
},
|
|
}}
|
|
svc := NewService(repo, &fakeCache{}, time.Minute, 200)
|
|
|
|
res, err := svc.ValidateAnswerByQuestionID(context.Background(), "q1", "Paris")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Matched {
|
|
t.Fatalf("expected match, got false score=%f", res.Score)
|
|
}
|
|
if res.Threshold != 0.85 {
|
|
t.Fatalf("unexpected threshold %f", res.Threshold)
|
|
}
|
|
}
|
|
|
|
// TestValidateAnswerByQuestionID_ValidationError returns validation error for empty provided answers.
|
|
func TestValidateAnswerByQuestionID_ValidationError(t *testing.T) {
|
|
repo := &fakeRepo{items: []*domain.Question{
|
|
{
|
|
ID: "q1",
|
|
Answer: "paris",
|
|
Difficulty: domain.DifficultyEasy,
|
|
IsActive: true,
|
|
},
|
|
}}
|
|
svc := NewService(repo, &fakeCache{}, time.Minute, 200)
|
|
|
|
_, err := svc.ValidateAnswerByQuestionID(context.Background(), "q1", "")
|
|
if !errors.Is(err, domain.ErrValidationFailed) {
|
|
t.Fatalf("expected validation error, got %v", err)
|
|
}
|
|
}
|