Files
sm-quizzes-plugin/admin_test.go
Jakub Zych 2ad4e8b1ec feat: review open answers in the admin and pick a grader per quiz
- Do oceny admin screen (golem15.quizzes.reviews) over the open answers,
  with a read-only context partial, score and comment virtual fields saved
  through SetScore, confirm and regrade record actions, bulk confirm
- open question type, rubric and max_length in the question form; grader
  dropdown on the quiz form from the grader registry
- Review model with quiz, status and student filters
2026-11-09 18:31:19 +01:00

134 lines
5.0 KiB
Go

package quizzes_test
import (
"slices"
"strings"
"testing"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"gorm.io/gorm"
"gorm.io/gorm/utils/tests"
)
func TestAdminCompiles(t *testing.T) {
app, plugins := activate(t, nil, nil)
routes, err := cabana.Activate(app, plugins)
if err != nil {
t.Fatalf("cabana.Activate: %v", err)
}
if routes == nil {
t.Fatal("no admin routes")
}
p := quizzesPlugin(t, plugins)
fsys := p.(pact.AdminAssets).AdminFS()
var ids []string
for _, ctl := range p.(pact.HasAdminControllers).AdminControllers() {
ids = append(ids, ctl.ID())
if _, err := cabana.CompileList(p.ID(), ctl, fsys); err != nil {
t.Errorf("%s list: %v", ctl.ID(), err)
}
_, formErr := cabana.CompileForm(p.ID(), ctl, fsys)
if ctl.ID() == "golem15.quizzes.attempts" {
if formErr == nil {
t.Error("attempts has a form; it must stay read-only")
}
} else if formErr != nil {
t.Errorf("%s form: %v", ctl.ID(), formErr)
}
}
slices.Sort(ids)
want := []string{"golem15.quizzes.answers", "golem15.quizzes.attempts", "golem15.quizzes.questions", "golem15.quizzes.quizzes", "golem15.quizzes.reviews"}
if !slices.Equal(ids, want) {
t.Errorf("controllers = %v, want %v", ids, want)
}
editor := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"golem15.quizzes.access_quizzes": true}}
for _, ctl := range p.(pact.HasAdminControllers).AdminControllers() {
perms := ctl.(pact.AdminPermissioned).RequiredPermissions()
allowed := cabana.Allows(editor, perms)
if wantAllowed := ctl.ID() != "golem15.quizzes.attempts"; allowed != wantAllowed {
t.Errorf("%s allowed for a quiz editor = %v, want %v", ctl.ID(), allowed, wantAllowed)
}
}
nav := p.(pact.HasNavigation).Navigation()
if len(nav) != 1 || nav[0].Label != "golem15.quizzes::lang.plugin.name" || len(nav[0].SideMenu) != 5 {
t.Errorf("navigation = %+v", nav)
} else {
var codes []string
for _, item := range nav[0].SideMenu {
codes = append(codes, item.Code)
}
if !slices.Equal(codes, []string{"quizzes", "questions", "answers", "reviews", "attempts"}) {
t.Errorf("side menu = %v, want reviews after answers", codes)
}
if review := nav[0].SideMenu[3]; review.Controller != "golem15.quizzes.reviews" || !slices.Equal(review.Permissions, []string{"golem15.quizzes.access_quizzes"}) {
t.Errorf("reviews entry = %+v", review)
}
}
}
func TestQuestionFilterScopes(t *testing.T) {
q := models.Question{}
if got := q.FilterOptions(models.ScopeByType); len(got) != 4 || got[3].Value != models.TypeOpen {
t.Errorf("type options = %v", got)
}
if got := q.FilterScope("unknown", nil, "x"); got != nil {
t.Errorf("FilterScope on a nil db = %v", got)
}
}
func TestReviewFilterScopes(t *testing.T) {
r := models.Review{}
if got := r.FilterScopes(); !slices.Equal(got, []string{models.ScopeByQuiz, models.ScopeByStatus, models.ScopeByStudent}) {
t.Errorf("FilterScopes = %v", got)
}
opts := r.FilterOptions(models.ScopeByStatus)
var values []string
for _, o := range opts {
values = append(values, o.Value)
if o.Label != "golem15.quizzes::lang.reviews.status_"+o.Value {
t.Errorf("status %s label = %q", o.Value, o.Label)
}
}
if !slices.Equal(values, []string{"pending", "graded", "failed", "reviewed"}) {
t.Errorf("status options = %v", values)
}
if got := r.FilterScope("unknown", nil, "x"); got != nil {
t.Errorf("FilterScope on a nil db = %v", got)
}
db, err := gorm.Open(tests.DummyDialector{}, &gorm.Config{DryRun: true})
if err != nil {
t.Fatalf("dry-run db: %v", err)
}
sqlOf := func(scope string, value any) string {
var rows []models.Review
stmt := r.FilterScope(scope, db.Session(&gorm.Session{DryRun: true}).Model(&models.Review{}), value).Find(&rows).Statement
return stmt.SQL.String()
}
if got := sqlOf(models.ScopeByStatus, []any{"graded"}); !strings.Contains(got, "golem15_quizzes_attempt_answers.grading_status IN") {
t.Errorf("status SQL = %s", got)
}
if got := sqlOf(models.ScopeByStatus, []any{"bogus"}); !strings.Contains(got, "1 = 0") {
t.Errorf("unknown status SQL = %s, want no rows", got)
}
if got := sqlOf(models.ScopeByQuiz, []any{"3"}); !strings.Contains(got, "question_id IN (SELECT id FROM golem15_quizzes_questions WHERE quiz_id IN") {
t.Errorf("quiz SQL = %s", got)
}
if got := sqlOf(models.ScopeByStudent, []any{"7"}); !strings.Contains(got, "attempt_id IN (SELECT id FROM golem15_quizzes_attempts WHERE user_id IN") {
t.Errorf("student SQL = %s", got)
}
if got := sqlOf(models.ScopeByQuiz, nil); strings.Contains(got, "question_id IN") {
t.Errorf("empty value SQL = %s, want no filter", got)
}
if name, err := lagoon.MorphType(nil, &models.Review{}); err != nil || name == "golem15_quizzes_attempt_answers" {
t.Errorf("Review morph type = %q, %v; it must differ from AttemptAnswer's table", name, err)
}
}