Categories move from a hard-coded list to a feedback_categories table (migration 0062), seeded with the four existing keys, labels and descriptions so production feedback is untouched; any other key already in use gets an archived category. Keys are fixed, so renames relabel past feedback. Archive hides a category from the form; only unused categories can be deleted. Submissions are validated against active categories, and feedback responses carry category_label. New: GET /feedback/categories (public); /admin/feedback/categories CRUD + reorder behind the new feedback:categories permission (granted to Admin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.6 KiB
Go
107 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Categories are managed from the admin: a new one is offered on the form
|
|
// and accepts feedback; renaming relabels past feedback; archiving stops new
|
|
// submissions but keeps the label; in-use categories can't be deleted.
|
|
func TestFeedbackCategories(t *testing.T) {
|
|
_, _, pg := testChatService(t)
|
|
ctx := context.Background()
|
|
repo := repository.NewFeedbackCategoryRepo(pg)
|
|
cats := NewFeedbackCategoryService(repo)
|
|
fb := NewFeedbackService(repository.NewFeedbackRepo(pg), repo, nil, nil, zap.NewNop())
|
|
user := chatUser(t, pg, "Feedback")
|
|
t.Cleanup(func() {
|
|
bg := context.Background()
|
|
_, _ = pg.Pool.Exec(bg, `DELETE FROM feedback WHERE user_id = $1`, user)
|
|
_, _ = pg.Pool.Exec(bg, `DELETE FROM feedback_categories WHERE key LIKE 'zz_qa_audio%'`)
|
|
})
|
|
|
|
// The seeded categories production already uses are all on the form.
|
|
active, err := cats.Active(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, c := range active {
|
|
seen[c.Key] = true
|
|
}
|
|
for _, k := range []string{"general", "bug", "feature", "other"} {
|
|
if !seen[k] {
|
|
t.Errorf("seeded category %q missing from the form", k)
|
|
}
|
|
}
|
|
|
|
// Keys come from the name, and never collide.
|
|
c1, err := cats.Create(ctx, "ZZ QA Audio problem", "Playback trouble")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c2, err := cats.Create(ctx, "ZZ QA audio problem!", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c1.Key != "zz_qa_audio_problem" || c2.Key != "zz_qa_audio_problem_2" {
|
|
t.Errorf("keys = %q, %q", c1.Key, c2.Key)
|
|
}
|
|
if _, err := cats.Create(ctx, " ", ""); !errors.Is(err, ErrCategoryLabel) {
|
|
t.Errorf("blank name: %v, want ErrCategoryLabel", err)
|
|
}
|
|
|
|
// New categories take feedback; unknown ones don't.
|
|
if _, err := fb.Submit(ctx, &user, c1.Key, "the audio stutters", nil); err != nil {
|
|
t.Fatalf("submit in a new category: %v", err)
|
|
}
|
|
if _, err := fb.Submit(ctx, &user, "no_such_category", "x", nil); !errors.Is(err, ErrFeedbackCategory) {
|
|
t.Errorf("unknown category: %v, want ErrFeedbackCategory", err)
|
|
}
|
|
|
|
// Renaming relabels what's already been filed.
|
|
if err := cats.Update(ctx, c1.Key, "ZZ QA Audio & playback", "Playback trouble", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mine, _ := fb.ListForUser(ctx, user, 10)
|
|
if len(mine) != 1 || mine[0].CategoryLabel != "ZZ QA Audio & playback" || mine[0].Category != c1.Key {
|
|
t.Errorf("past feedback after rename = %+v", mine)
|
|
}
|
|
|
|
// Archived: off the form, refuses new feedback, keeps its label.
|
|
if err := cats.Update(ctx, c1.Key, "ZZ QA Audio & playback", "", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := fb.Submit(ctx, &user, c1.Key, "again", nil); !errors.Is(err, ErrFeedbackCategory) {
|
|
t.Errorf("submit to an archived category: %v, want ErrFeedbackCategory", err)
|
|
}
|
|
active, _ = cats.Active(ctx)
|
|
for _, c := range active {
|
|
if c.Key == c1.Key {
|
|
t.Error("archived category still on the form")
|
|
}
|
|
}
|
|
if mine, _ := fb.ListForUser(ctx, user, 10); mine[0].CategoryLabel != "ZZ QA Audio & playback" {
|
|
t.Error("archiving lost the label on past feedback")
|
|
}
|
|
all, _ := cats.All(ctx)
|
|
for _, c := range all {
|
|
if c.Key == c1.Key && (c.ArchivedAt == nil || c.UsageCount != 1) {
|
|
t.Errorf("admin view of archived category = %+v", c)
|
|
}
|
|
}
|
|
|
|
// In use → can't be deleted; unused → can.
|
|
if err := cats.Delete(ctx, c1.Key); !errors.Is(err, repository.ErrFeedbackCategoryInUse) {
|
|
t.Errorf("deleting an in-use category: %v, want ErrFeedbackCategoryInUse", err)
|
|
}
|
|
if err := cats.Delete(ctx, c2.Key); err != nil {
|
|
t.Errorf("deleting an unused category: %v", err)
|
|
}
|
|
}
|