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

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