Full-stack F# (Domain/Server/Client via Fable+Elmish+Feliz), PostgreSQL persistence via Dapper, Docker Compose deployment. Student quiz-taking flow with time-limit enforcement and focus-loss tracking, Teacher question bank and quiz builder with results analytics, Admin user management.
99 lines
3.6 KiB
Forth
99 lines
3.6 KiB
Forth
module Domain.Tests.ValidationTests
|
|
|
|
open System
|
|
open Xunit
|
|
open Domain
|
|
|
|
let private newQuestion (qType: QuestionType) : Question =
|
|
{ Id = QuestionId(Guid.NewGuid())
|
|
TopicId = TopicId(Guid.NewGuid())
|
|
Text = "Sample?"
|
|
Points = 1.0
|
|
Type = qType }
|
|
|
|
[<Fact>]
|
|
let ``single choice: correct option must be among the listed options`` () =
|
|
let optA = { Id = OptionId(Guid.NewGuid()); Text = "A" }
|
|
let optB = { Id = OptionId(Guid.NewGuid()); Text = "B" }
|
|
let foreignId = OptionId(Guid.NewGuid())
|
|
let question = newQuestion (SingleChoice([ optA; optB ], foreignId))
|
|
|
|
match QuestionValidation.validate question with
|
|
| Error errors -> Assert.Contains("Correct option must be one of the provided options", errors)
|
|
| Ok _ -> Assert.Fail "expected validation error"
|
|
|
|
[<Fact>]
|
|
let ``multiple choice: correct set must be subset of options`` () =
|
|
let optA = { Id = OptionId(Guid.NewGuid()); Text = "A" }
|
|
let optB = { Id = OptionId(Guid.NewGuid()); Text = "B" }
|
|
let foreignId = OptionId(Guid.NewGuid())
|
|
let question = newQuestion (MultipleChoice([ optA; optB ], Set.ofList [ optA.Id; foreignId ]))
|
|
|
|
match QuestionValidation.validate question with
|
|
| Error errors -> Assert.Contains("Correct options must be a subset of the provided options", errors)
|
|
| Ok _ -> Assert.Fail "expected validation error"
|
|
|
|
[<Fact>]
|
|
let ``numeric: negative tolerance is invalid`` () =
|
|
let question = newQuestion (Numeric(1.0, -0.5))
|
|
|
|
match QuestionValidation.validate question with
|
|
| Error errors -> Assert.Contains("Tolerance must not be negative", errors)
|
|
| Ok _ -> Assert.Fail "expected validation error"
|
|
|
|
[<Fact>]
|
|
let ``valid question passes validation`` () =
|
|
let optA = { Id = OptionId(Guid.NewGuid()); Text = "A" }
|
|
let optB = { Id = OptionId(Guid.NewGuid()); Text = "B" }
|
|
let question = newQuestion (SingleChoice([ optA; optB ], optA.Id))
|
|
|
|
match QuestionValidation.validate question with
|
|
| Ok _ -> ()
|
|
| Error errors -> Assert.Fail(String.concat "; " errors)
|
|
|
|
[<Fact>]
|
|
let ``quiz: must contain at least one question`` () =
|
|
let quiz: Quiz =
|
|
{ Id = QuizId(Guid.NewGuid())
|
|
OwnerId = UserId(Guid.NewGuid())
|
|
Title = "Empty quiz"
|
|
Description = ""
|
|
TimeLimit = None
|
|
MaxAttempts = None
|
|
GradingMethod = HighestAttempt
|
|
ShuffleQuestions = false
|
|
ShuffleAnswers = false
|
|
OpenFrom = None
|
|
OpenTo = None
|
|
PassingScore = None
|
|
QuestionSources = []
|
|
AssignedStudentIds = Set.empty }
|
|
|
|
match QuizValidation.validate quiz with
|
|
| Error errors -> Assert.Contains("Quiz must contain at least one question", errors)
|
|
| Ok _ -> Assert.Fail "expected validation error"
|
|
|
|
[<Fact>]
|
|
let ``quiz: OpenFrom must precede OpenTo`` () =
|
|
let now = DateTimeOffset.UtcNow
|
|
|
|
let quiz: Quiz =
|
|
{ Id = QuizId(Guid.NewGuid())
|
|
OwnerId = UserId(Guid.NewGuid())
|
|
Title = "Backwards window"
|
|
Description = ""
|
|
TimeLimit = None
|
|
MaxAttempts = None
|
|
GradingMethod = HighestAttempt
|
|
ShuffleQuestions = false
|
|
ShuffleAnswers = false
|
|
OpenFrom = Some(now.AddDays 1.0)
|
|
OpenTo = Some now
|
|
PassingScore = None
|
|
QuestionSources = [ FixedQuestion { QuestionId = QuestionId(Guid.NewGuid()); Points = 1.0; Order = 0 } ]
|
|
AssignedStudentIds = Set.empty }
|
|
|
|
match QuizValidation.validate quiz with
|
|
| Error errors -> Assert.Contains("OpenFrom must be before OpenTo", errors)
|
|
| Ok _ -> Assert.Fail "expected validation error"
|