Initial commit: standalone quiz-testing system

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.
This commit is contained in:
danamir
2026-08-06 12:36:16 +03:00
commit 942dfc9c1a
134 changed files with 10712 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
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"