diff --git a/test/cadet/assessments/assessments_test.exs b/test/cadet/assessments/assessments_test.exs index cd86b020a..22001ffd8 100644 --- a/test/cadet/assessments/assessments_test.exs +++ b/test/cadet/assessments/assessments_test.exs @@ -3368,6 +3368,268 @@ defmodule Cadet.AssessmentsTest do end end + describe "fetch_contest_voting_assesment_id function" do + test "correctly fetches voting assessment id when contest exists" do + course = insert(:course) + config = insert(:assessment_config) + + contest_assessment = insert(:assessment, %{course: course, config: config}) + voting_assessment = insert(:assessment, %{course: course, config: config}) + + contest_question = insert(:programming_question, assessment: contest_assessment) + + voting_question = + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_assessment.number) + }) + + result = Assessments.fetch_contest_voting_assesment_id(voting_assessment.id) + + assert result == contest_assessment.id + end + + test "returns nil when assessment does not exist" do + non_existent_id = 999_999 + result = Assessments.fetch_contest_voting_assesment_id(non_existent_id) + assert is_nil(result) + end + + test "returns nil when no contest number matches" do + course = insert(:course) + config = insert(:assessment_config) + + voting_assessment = insert(:assessment, %{course: course, config: config}) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: "non_existent_number") + }) + + result = Assessments.fetch_contest_voting_assesment_id(voting_assessment.id) + + assert is_nil(result) + end + end + + describe "fetch_all_contests function" do + test "fetches all contests for a course" do + course = insert(:course) + config = insert(:assessment_config, %{type: "Contests"}) + + contest_assessment_1 = + insert(:assessment, %{ + course: course, + config: config, + is_published: true, + title: "Contest 1" + }) + + contest_assessment_2 = + insert(:assessment, %{ + course: course, + config: config, + is_published: false, + title: "Contest 2" + }) + + voting_assessment = insert(:assessment, %{course: course}) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_assessment_1.number) + }) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_assessment_2.number) + }) + + result = Assessments.fetch_all_contests(course.id) + + assert length(result) == 2 + + assert Enum.find(result, fn c -> c.contest_id == contest_assessment_1.id end).published == + true + + assert Enum.find(result, fn c -> c.contest_id == contest_assessment_2.id end).published == + false + end + + test "returns empty list when no voting questions exist" do + course = insert(:course) + result = Assessments.fetch_all_contests(course.id) + assert result == [] + end + + test "excludes contests that are not of type Contests" do + course = insert(:course) + non_contest_config = insert(:assessment_config, %{type: "Mission"}) + + non_contest_assessment = + insert(:assessment, %{ + course: course, + config: non_contest_config, + is_published: true + }) + + voting_assessment = insert(:assessment, %{course: course}) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: non_contest_assessment.number) + }) + + result = Assessments.fetch_all_contests(course.id) + + assert result == [] + end + end + + describe "fetch_top_relative_score_answers ranking" do + test "correctly ranks answers with RANK() OVER function" do + course = insert(:course) + config = insert(:assessment_config) + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_question = insert(:programming_question, assessment: contest_assessment) + voting_assessment = insert(:assessment, %{course: course, config: config}) + voting_question = insert(:voting_question, assessment: voting_assessment) + + # generate 5 students with answers having different relative scores + student_list = insert_list(5, :course_registration, %{course: course, role: :student}) + + submission_list = + Enum.map( + student_list, + fn student -> + insert( + :submission, + student: student, + assessment: contest_assessment, + status: "submitted" + ) + end + ) + + ans_list = + Enum.map( + Enum.with_index(submission_list), + fn {submission, index} -> + insert( + :answer, + answer: build(:programming_answer), + submission: submission, + question: contest_question, + relative_score: 10 - index + ) + end + ) + + top_2 = Assessments.fetch_top_relative_score_answers(contest_question.id, 2) + + assert length(top_2) == 2 + assert Enum.all?(top_2, fn ans -> ans.rank <= 2 end) + assert Enum.map(top_2, fn ans -> ans.relative_score end) == [10.0, 9.0] + end + + test "handles tied scores correctly with RANK() OVER" do + course = insert(:course) + config = insert(:assessment_config) + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_question = insert(:programming_question, assessment: contest_assessment) + voting_assessment = insert(:assessment, %{course: course, config: config}) + voting_question = insert(:voting_question, assessment: voting_assessment) + + student_list = insert_list(3, :course_registration, %{course: course, role: :student}) + + submission_list = + Enum.map( + student_list, + fn student -> + insert( + :submission, + student: student, + assessment: contest_assessment, + status: "submitted" + ) + end + ) + + # Two answers with same relative_score (tied for first) + insert(:answer, %{ + answer: build(:programming_answer), + submission: Enum.at(submission_list, 0), + question: contest_question, + relative_score: 10.0 + }) + + insert(:answer, %{ + answer: build(:programming_answer), + submission: Enum.at(submission_list, 1), + question: contest_question, + relative_score: 10.0 + }) + + insert(:answer, %{ + answer: build(:programming_answer), + submission: Enum.at(submission_list, 2), + question: contest_question, + relative_score: 9.0 + }) + + top_2 = Assessments.fetch_top_relative_score_answers(contest_question.id, 2) + + assert length(top_2) == 2 + assert Enum.count(top_2, fn ans -> ans.rank == 1 end) == 2 + end + end + + describe "fetch_top_popular_score_answers ranking" do + test "correctly ranks answers with RANK() OVER function" do + course = insert(:course) + config = insert(:assessment_config) + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_question = insert(:programming_question, assessment: contest_assessment) + voting_assessment = insert(:assessment, %{course: course, config: config}) + voting_question = insert(:voting_question, assessment: voting_assessment) + + student_list = insert_list(5, :course_registration, %{course: course, role: :student}) + + submission_list = + Enum.map( + student_list, + fn student -> + insert( + :submission, + student: student, + assessment: contest_assessment, + status: "submitted" + ) + end + ) + + ans_list = + Enum.map( + Enum.with_index(submission_list), + fn {submission, index} -> + insert( + :answer, + answer: build(:programming_answer), + submission: submission, + question: contest_question, + popular_score: 20 - index + ) + end + ) + + top_3 = Assessments.fetch_top_popular_score_answers(contest_question.id, 3) + + assert length(top_3) == 3 + assert Enum.all?(top_3, fn ans -> ans.rank <= 3 end) + assert Enum.map(top_3, fn ans -> ans.popular_score end) == [20.0, 19.0, 18.0] + end + end + defp get_answer_relative_scores(answers) do answers |> Enum.map(fn ans -> ans.relative_score end) end diff --git a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs index b1cfbde5a..dc363ac0d 100644 --- a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs @@ -159,6 +159,85 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end + describe "POST /assessments/:assessmentid/calculateContestScore" do + @tag authenticate: :admin + test "successfully calculates contest score", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_students = insert_list(5, :course_registration, %{course: course, role: :student}) + contest_question = insert(:programming_question, %{assessment: contest_assessment}) + + contest_submissions = + contest_students + |> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1})) + + _contest_answers = + contest_submissions + |> Enum.map( + &insert(:answer, %{ + question: contest_question, + submission: &1, + answer: build(:programming_answer) + }) + ) + + voting_assessment = insert(:assessment, %{course: course, config: config}) + + voting_question = + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_assessment.number) + }) + + conn + |> post( + "/v2/courses/#{course.id}/admin/assessments/#{voting_assessment.id}/calculateContestScore" + ) + |> response(200) + end + end + + describe "POST /assessments/:assessmentid/dispatchContestXp" do + @tag authenticate: :admin + test "successfully dispatches xp to contest winners", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_students = insert_list(5, :course_registration, %{course: course, role: :student}) + contest_question = insert(:programming_question, %{assessment: contest_assessment}) + + contest_submissions = + contest_students + |> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1})) + + _contest_answers = + contest_submissions + |> Enum.map( + &insert(:answer, %{ + question: contest_question, + submission: &1, + popular_score: 10.0, + relative_score: 10.0, + answer: build(:programming_answer) + }) + ) + + voting_assessment = insert(:assessment, %{course: course, config: config}) + voting_question = insert(:voting_question, %{assessment: voting_assessment}) + + conn + |> post( + "/v2/courses/#{course.id}/admin/assessments/#{voting_assessment.id}/dispatchContestXp" + ) + |> response(200) + end + end + describe "POST /, unauthenticated" do test "unauthorized", %{ conn: conn, diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 17e7cca72..e78644998 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -1958,6 +1958,196 @@ defmodule CadetWeb.AssessmentsControllerTest do end end + describe "GET /combined_total_xp_for_all_users" do + @tag authenticate: :student + test "unauthorized for student role", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + + conn + |> get("/v2/courses/#{course.id}/all_users_xp") + |> response(401) + end + end + + describe "GET /paginated_total_xp_for_leaderboard_display" do + @tag authenticate: :student + test "pagination with offset and limit", %{conn: conn} do + user = conn.assigns[:current_user] + + test_cr = + insert(:course_registration, %{course: insert(:course), role: :student, user: user}) + + conn = assign(conn, :test_cr, test_cr) + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + question = insert(:programming_question, assessment: assessment) + + # Create 10 students with different XP amounts + student_list = insert_list(10, :course_registration, %{course: course, role: :student}) + + submission_list = + Enum.map( + student_list, + fn student -> + insert( + :submission, + student: student, + assessment: assessment, + status: "submitted", + is_grading_published: true + ) + end + ) + + _ans_list = + Enum.map( + Enum.with_index(submission_list), + fn {submission, index} -> + insert( + :answer, + answer: build(:programming_answer), + submission: submission, + question: question, + xp: 100 + index * 10 + ) + end + ) + + # Test with offset and limit + conn = get(conn, "/v2/courses/#{course.id}/get_paginated_display?offset=0&page_size=5") + response = json_response(conn, 200) + + assert response["total_count"] == 10 + assert length(response["users"]) == 5 + end + end + + describe "GET /get_all_contests" do + @tag authenticate: :student + test "retrieves all contests for a course", %{conn: conn} do + user = conn.assigns[:current_user] + + test_cr = + insert(:course_registration, %{course: insert(:course), role: :student, user: user}) + + conn = assign(conn, :test_cr, test_cr) + course = test_cr.course + config = insert(:assessment_config, %{type: "Contests", course: course}) + + contest_1 = + insert(:assessment, %{ + course: course, + config: config, + is_published: true, + title: "Contest 1" + }) + + contest_2 = + insert(:assessment, %{ + course: course, + config: config, + is_published: false, + title: "Contest 2" + }) + + voting_assessment = insert(:assessment, %{course: course}) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_1.number) + }) + + insert(:voting_question, %{ + assessment: voting_assessment, + question: build(:voting_question_content, contest_number: contest_2.number) + }) + + response = + conn + |> get("/v2/courses/#{course.id}/all_contests") + |> json_response(200) + + assert length(response) == 2 + assert Enum.any?(response, fn c -> c["title"] == "Contest 1" and c["published"] end) + assert Enum.any?(response, fn c -> c["title"] == "Contest 2" and not c["published"] end) + end + + @tag authenticate: :student + test "returns empty list when no contests exist", %{conn: conn} do + user = conn.assigns[:current_user] + + test_cr = + insert(:course_registration, %{course: insert(:course), role: :student, user: user}) + + conn = assign(conn, :test_cr, test_cr) + course = test_cr.course + + response = + conn + |> get("/v2/courses/#{course.id}/all_contests") + |> json_response(200) + + assert response == [] + end + end + + describe "GET /:assessment_id/scoreLeaderboard with custom visible_entries" do + @tag authenticate: :student + test "uses visible_entries parameter when provided", %{conn: conn} do + user = conn.assigns[:current_user] + + test_cr = + insert(:course_registration, %{course: insert(:course), role: :student, user: user}) + + conn = assign(conn, :test_cr, test_cr) + course = test_cr.course + + config = insert(:assessment_config, %{course: course}) + contest_assessment = insert(:assessment, %{course: course, config: config}) + contest_students = insert_list(10, :course_registration, %{course: course, role: :student}) + contest_question = insert(:programming_question, %{assessment: contest_assessment}) + + contest_submissions = + contest_students + |> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1})) + + _contest_answer = + contest_submissions + |> Enum.map( + &insert(:answer, %{ + question: contest_question, + submission: &1, + relative_score: 10.0, + answer: build(:programming_answer) + }) + ) + + voting_assessment = insert(:assessment, %{course: course, config: config}) + + insert( + :voting_question, + %{ + question: build(:voting_question_content, contest_number: contest_assessment.number), + assessment: voting_assessment + } + ) + + params = %{"visible_entries" => 5} + + resp = + conn + |> get( + "/v2/courses/#{course.id}/assessments/#{voting_assessment.id}/scoreLeaderboard?visible_entries=5" + ) + |> json_response(200) + + assert length(resp["leaderboard"]) <= 5 + assert Enum.all?(resp["leaderboard"], fn entry -> Map.has_key?(entry, "rank") end) + end + end + defp build_url(course_id), do: "/v2/courses/#{course_id}/assessments/" defp build_url(course_id, assessment_id),