add questions

This commit is contained in:
2026-09-01 01:01:37 +07:00
parent 4431a4d491
commit 69666ee264
23 changed files with 4151 additions and 368 deletions
+86 -1
View File
@@ -295,6 +295,75 @@ func (s *ScenarioService) UpdateScenarioIntro(
return s.updateStory(ctx, id, story)
}
// AddScenarioQuestion добавляет вопрос дела в конец списка вопросов сценария.
func (s *ScenarioService) AddScenarioQuestion(
ctx context.Context,
id int,
question *storytelling.Question,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
}
story.Questions = append(story.Questions, question)
return s.updateStory(ctx, id, story)
}
// UpdateScenarioQuestion обновляет вопрос дела по его коду. Смена кода
// оставляет строки team_answers старого кода в БД
func (s *ScenarioService) UpdateScenarioQuestion(
ctx context.Context,
id int,
code string,
question *storytelling.Question,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
}
for i := range story.Questions {
if story.Questions[i].Code == code {
story.Questions[i] = question
break
}
}
return s.updateStory(ctx, id, story)
}
// DeleteScenarioQuestion удаляет вопрос дела по его коду.
func (s *ScenarioService) DeleteScenarioQuestion(
ctx context.Context,
id int,
code string,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
}
for i := range story.Questions {
if story.Questions[i].Code == code {
story.Questions = append(story.Questions[:i], story.Questions[i+1:]...)
break
}
}
return s.updateStory(ctx, id, story)
}
func (s *ScenarioService) getStory(ctx context.Context, id int) (*storytelling.Story, error) {
storyString, err := s.scenariosRepo.GetStoryByScenarioID(ctx, id)
if err != nil {
@@ -432,7 +501,6 @@ func (s *ScenarioService) UploadArchive(
return id, nil
}
func (s *ScenarioService) updateStory(ctx context.Context, id int, story *storytelling.Story) error {
for _, place := range story.Places {
place.Image = strings.TrimPrefix(place.Image, s.domain)
@@ -478,6 +546,23 @@ func normalizeStory(story *storytelling.Story) (string, error) {
}
story.Places = cleanPlaces
// Коды вопросов уникальны, пустые коды отбрасываются (как у точек).
questionCodes := map[string]struct{}{}
for _, question := range story.Questions {
questionCodes[question.Code] = struct{}{}
}
if len(questionCodes) != len(story.Questions) {
return "", errors.New("Такой код вопроса уже существует")
}
cleanQuestions := make([]*storytelling.Question, 0, len(story.Questions))
for _, question := range story.Questions {
if question.Code == "" {
continue
}
cleanQuestions = append(cleanQuestions, question)
}
story.Questions = cleanQuestions
return convertStory(story)
}
@@ -686,3 +686,132 @@ func mustJSON(t *testing.T, v scenario_archive.ScenarioJSON) []byte {
}
return b
}
func TestScenarioQuestionsCRUD(t *testing.T) {
repo := newFakeScenariosRepo()
storage := newFakeStorage()
seedScenario(t, repo, storage)
svc := newTestService(repo, storage)
// Добавление вопроса автором.
err := svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{
Code: "q1",
Text: "Кто убийца?",
Answer: "Дворецкий",
}, 7, false)
if err != nil {
t.Fatalf("AddScenarioQuestion: %v", err)
}
err = svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{
Code: "q2",
Text: "Орудие?",
Answer: "Канделябр",
}, 7, false)
if err != nil {
t.Fatalf("AddScenarioQuestion: %v", err)
}
full, err := svc.GetFullScenarioByID(context.Background(), 1)
if err != nil {
t.Fatalf("GetFullScenarioByID: %v", err)
}
if len(full.Story.Questions) != 2 {
t.Fatalf("len(Questions) = %d, want 2", len(full.Story.Questions))
}
// Вопросы добавляются в конец списка: q1 добавлен первым.
if full.Story.Questions[0].Code != "q1" || full.Story.Questions[0].Answer != "Дворецкий" {
t.Errorf("Questions[0] = %+v, want q1 с правильным ответом", full.Story.Questions[0])
}
if full.Story.Questions[1].Code != "q2" || full.Story.Questions[1].Answer != "Канделябр" {
t.Errorf("Questions[1] = %+v, want q2 с правильным ответом", full.Story.Questions[1])
}
// Обновление вопроса по коду.
err = svc.UpdateScenarioQuestion(context.Background(), 1, "q1", &storytelling.Question{
Code: "q1",
Text: "Кто убийца на самом деле?",
Answer: "Экономка",
}, 7, false)
if err != nil {
t.Fatalf("UpdateScenarioQuestion: %v", err)
}
full, err = svc.GetFullScenarioByID(context.Background(), 1)
if err != nil {
t.Fatalf("GetFullScenarioByID: %v", err)
}
found := false
for _, q := range full.Story.Questions {
if q.Code == "q1" {
found = true
if q.Text != "Кто убийца на самом деле?" || q.Answer != "Экономка" {
t.Errorf("q1 = %+v, want обновлённый вопрос", q)
}
}
}
if !found {
t.Error("вопрос q1 не найден после обновления")
}
// Удаление вопроса.
err = svc.DeleteScenarioQuestion(context.Background(), 1, "q2", 7, false)
if err != nil {
t.Fatalf("DeleteScenarioQuestion: %v", err)
}
full, err = svc.GetFullScenarioByID(context.Background(), 1)
if err != nil {
t.Fatalf("GetFullScenarioByID: %v", err)
}
if len(full.Story.Questions) != 1 || full.Story.Questions[0].Code != "q1" {
t.Errorf("Questions = %+v, want только q1", full.Story.Questions)
}
// Чужой автор не может менять вопросы.
err = svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{Code: "q3"}, 99, false)
if !errors.Is(err, ErrScenarioNotOwner) {
t.Errorf("AddScenarioQuestion чужой автор: err = %v, want ErrScenarioNotOwner", err)
}
err = svc.DeleteScenarioQuestion(context.Background(), 1, "q1", 99, false)
if !errors.Is(err, ErrScenarioNotOwner) {
t.Errorf("DeleteScenarioQuestion чужой автор: err = %v, want ErrScenarioNotOwner", err)
}
// Админ может менять вопросы чужого сценария.
err = svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{
Code: "q3", Text: "Вопрос админа",
}, 1, true)
if err != nil {
t.Fatalf("AddScenarioQuestion админ: %v", err)
}
}
func TestNormalizeStoryQuestionCodes(t *testing.T) {
repo := newFakeScenariosRepo()
storage := newFakeStorage()
seedScenario(t, repo, storage)
svc := newTestService(repo, storage)
// Дубликат кода вопроса — ошибка.
err := svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{Code: "q1"}, 7, false)
if err != nil {
t.Fatalf("AddScenarioQuestion q1: %v", err)
}
err = svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{Code: "q1"}, 7, false)
if err == nil || !strings.Contains(err.Error(), "Такой код вопроса уже существует") {
t.Errorf("AddScenarioQuestion с дублем кода: err = %v, want ошибку о дубле", err)
}
// Пустой код — вопрос отбрасывается при сохранении.
err = svc.AddScenarioQuestion(context.Background(), 1, &storytelling.Question{Code: "", Text: "без кода"}, 7, false)
if err != nil {
t.Fatalf("AddScenarioQuestion с пустым кодом: %v", err)
}
full, err := svc.GetFullScenarioByID(context.Background(), 1)
if err != nil {
t.Fatalf("GetFullScenarioByID: %v", err)
}
for _, q := range full.Story.Questions {
if q.Code == "" {
t.Error("вопрос с пустым кодом сохранился")
}
}
}