diff --git a/README.md b/README.md index 21040e105..5b7531b3d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ $ vim config/secrets.exs `luminus_redirect_url` are required for the application to properly authenticate with LumiNUS. - A valid `cs1101s_repository`, `cs1101s_rsa_key` is required for the application to run with the `--updater` flag. Otherwise, the default values will suffice. + - A valid `instance_id`, `key_id` and `key_secret` are required to use ChatKit's services. Otherwise, the placeholder values can be left as they are. 2. Install Elixir dependencies ```bash @@ -66,6 +67,14 @@ We recommend setting up nginx to handle preflight checks using the following If you do this, do remember to point cadet-frontend to port `4001` instead of `4000` +### Chatkit + +The chat functionality replacing the previous comment field found in assignments is built on top of Chatkit. Its documentation can be found [here](https://pusher.com/docs/chatkit). + +If you are using Chatkit, obtain your instance ID, key ID and secret key from your account, and set them in. Instructions to that are found [here](https://pusher.com/docs/chatkit/authentication#chatkit-key-and-instance-id). + +Internet connection is required for usage. + ### Style Guide diff --git a/config/config.exs b/config/config.exs index 95693c72a..ba910cc3f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -16,6 +16,8 @@ config :cadet, Cadet.Jobs.Scheduler, overlap: false, jobs: [ {"@hourly", {Mix.Tasks.Cadet.Assessments.Update, :run, [nil]}}, + # Create Chatkit rooms if they do not already exist at 1am + {"0 1 * * *", {Mix.Tasks.Cadet.ChatkitRoom, :run, [nil]}}, # Grade previous day's submission at 3am {"0 3 * * *", {Cadet.Autograder.GradingJob, :grade_all_due_yesterday, []}} ] diff --git a/config/secrets.exs.example b/config/secrets.exs.example index 0f792fdb7..53992ea46 100644 --- a/config/secrets.exs.example +++ b/config/secrets.exs.example @@ -13,6 +13,11 @@ config :cadet, ], autograder: [ lambda_name: "autograderLambdaName" + ], + chat: [ + instance_id: "CHATKIT_INSTANCE_ID", + key_id: "CHATKIT_KEY_ID", + key_secret: "CHATKIT_KEY_SECRET" ] config :sentry, diff --git a/lib/cadet/assessments/answer.ex b/lib/cadet/assessments/answer.ex index 9d72b280b..902077291 100644 --- a/lib/cadet/assessments/answer.ex +++ b/lib/cadet/assessments/answer.ex @@ -30,7 +30,7 @@ defmodule Cadet.Assessments.Answer do end @required_fields ~w(answer submission_id question_id type)a - @optional_fields ~w(xp xp_adjustment grade comment adjustment grader_id)a + @optional_fields ~w(xp xp_adjustment grade adjustment grader_id)a def changeset(answer, params) do answer @@ -49,7 +49,7 @@ defmodule Cadet.Assessments.Answer do answer |> cast( params, - ~w(grader_id xp xp_adjustment grade adjustment autograding_results autograding_status comment)a + ~w(grader_id xp xp_adjustment grade adjustment autograding_results autograding_status)a ) |> add_belongs_to_id_from_model(:grader, params) |> foreign_key_constraint(:grader_id) @@ -63,6 +63,12 @@ defmodule Cadet.Assessments.Answer do |> validate_xp_grade_adjustment_total() end + # TODO: add some validation + @spec comment_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() + def comment_changeset(answer, params) do + cast(answer, params, ~w(comment)a) + end + @spec validate_xp_grade_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp validate_xp_grade_adjustment_total(changeset) do answer = apply_changes(changeset) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index bd6729c3d..f97daebc3 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -10,6 +10,7 @@ defmodule Cadet.Assessments do alias Cadet.Accounts.User alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission} alias Cadet.Autograder.GradingJob + alias Cadet.Chat.Room alias Ecto.Multi @xp_early_submission_max_bonus 100 @@ -363,8 +364,13 @@ defmodule Cadet.Assessments do {:is_open?, true} <- is_open?(question.assessment), {:ok, submission} <- find_or_create_submission(user, question.assessment), {:status, true} <- {:status, submission.status != :submitted}, - {:ok, _} <- insert_or_update_answer(submission, question, raw_answer) do + {:ok, answer} <- insert_or_update_answer(submission, question, raw_answer) do update_submission_status(submission, question.assessment) + + if answer.comment == nil do + Room.create_rooms(submission, answer, user) + end + {:ok, nil} else {:question_found?, false} -> {:error, {:not_found, "Question not found"}} @@ -468,7 +474,6 @@ defmodule Cadet.Assessments do xp_adjustment: 0, autograding_status: :none, autograding_results: [], - comment: nil, grader_id: nil }) |> Repo.update()} diff --git a/lib/cadet/chat/room.ex b/lib/cadet/chat/room.ex new file mode 100644 index 000000000..e118ace35 --- /dev/null +++ b/lib/cadet/chat/room.ex @@ -0,0 +1,91 @@ +defmodule Cadet.Chat.Room do + @moduledoc """ + Contains logic pertaining to chatroom creation to supplement ChatKit, an external service engaged for Source Academy. + ChatKit's API can be found here: https://pusher.com/docs/chatkit + """ + + require Logger + + import Ecto.Query + + alias Cadet.Repo + alias Cadet.Assessments.{Answer, Submission} + alias Cadet.Accounts.User + alias Cadet.Chat.Token + + @instance_id (if Mix.env() != :test do + :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:instance_id) + else + "instance_id" + end) + + @doc """ + Creates a chatroom for every answer, and updates db with the chatroom id. + """ + def create_rooms( + %Submission{ + assessment_id: assessment_id + }, + answer = %Answer{question_id: question_id, comment: comment}, + user + ) do + with true <- comment == nil, + {:ok, %{"id" => room_id}} <- create_room(assessment_id, question_id, user) do + answer + |> Answer.comment_changeset(%{ + comment: room_id + }) + |> Repo.update() + end + end + + defp create_room( + assessment_id, + question_id, + %User{ + id: student_id, + nusnet_id: nusnet_id + } + ) do + HTTPoison.start() + + url = "https://us1.pusherplatform.io/services/chatkit/v4/#{@instance_id}/rooms" + + {:ok, token} = Token.get_superuser_token() + headers = [Authorization: "Bearer #{token}"] + + body = + Poison.encode!(%{ + "name" => "#{nusnet_id}_#{assessment_id}_Q#{question_id}", + "private" => true, + "user_ids" => get_staff_admin_user_ids() ++ [to_string(student_id)] + }) + + case HTTPoison.post(url, body, headers) do + {:ok, %HTTPoison.Response{body: body, status_code: 201}} -> + Poison.decode(body) + + {:ok, %HTTPoison.Response{body: body, status_code: status_code}} -> + response_body = Poison.decode!(body) + + Logger.error( + "Room creation failed: #{response_body["error"]}, " <> + "#{response_body["error_description"]} (status code #{status_code}) " <> + "[user_id: #{student_id}, assessment_id: #{assessment_id}, question_id: #{question_id}]" + ) + + {:error, nil} + + {:error, %HTTPoison.Error{reason: error}} -> + Logger.error("error: #{inspect(error, pretty: true)}") + {:error, nil} + end + end + + defp get_staff_admin_user_ids do + User + |> where([u], u.role in ^[:staff, :admin]) + |> Repo.all() + |> Enum.map(fn user -> to_string(user.id) end) + end +end diff --git a/lib/cadet/chat/token.ex b/lib/cadet/chat/token.ex new file mode 100644 index 000000000..0939ecae3 --- /dev/null +++ b/lib/cadet/chat/token.ex @@ -0,0 +1,50 @@ +defmodule Cadet.Chat.Token do + @moduledoc """ + Contains logic pertaining to the generation of tokens to supplement the usage of ChatKit, an external service engaged for Source Academy. + ChatKit's API can be found here: https://pusher.com/docs/chatkit + """ + + alias Cadet.Accounts.User + + @instance_id :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:instance_id) + @key_id :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:key_id) + @key_secret :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:key_secret) + @token_ttl 86_400 + @admin_user_id "admin" + + @doc """ + Generates user token for connection to ChatKit's ChatManager. + Returns {:ok, token, ttl}. + """ + def get_user_token(%User{id: user_id}) do + {:ok, token} = get_token(to_string(user_id)) + {:ok, token, @token_ttl} + end + + @doc """ + Generates a token for user with admin rights to enable superuser permissions. + Returns {:ok, token} + """ + def get_superuser_token do + get_token(@admin_user_id, true) + end + + defp get_token(user_id, su \\ false) when is_binary(user_id) do + curr_time_epoch = DateTime.to_unix(DateTime.utc_now()) + + payload = %{ + "instance" => @instance_id, + "iss" => "api_keys/#{@key_id}", + "exp" => curr_time_epoch + @token_ttl, + "iat" => curr_time_epoch, + "sub" => user_id, + "su" => su + } + + # Note: dialyzer says signing only returns {:ok, token} + Joken.Signer.sign( + payload, + Joken.Signer.create("HS256", @key_secret) + ) + end +end diff --git a/lib/cadet_web/controllers/chat_controller.ex b/lib/cadet_web/controllers/chat_controller.ex new file mode 100644 index 000000000..5e6318391 --- /dev/null +++ b/lib/cadet_web/controllers/chat_controller.ex @@ -0,0 +1,51 @@ +defmodule CadetWeb.ChatController do + @moduledoc """ + Provides token for connection to ChatKit's server. + Refer to ChatKit's API here: https://pusher.com/docs/chatkit + """ + + use CadetWeb, :controller + use PhoenixSwagger + + alias Cadet.Chat.Token + + def index(conn, _) do + user = conn.assigns.current_user + {:ok, token, ttl} = Token.get_user_token(user) + + render( + conn, + "index.json", + access_token: token, + expires_in: ttl + ) + end + + swagger_path :index do + post("/chat/token") + + summary("Get the ChatKit bearer token of a user. Token expires in 24 hours.") + + security([%{JWT: []}]) + + produces("application/json") + + response(200, "OK", Schema.ref(:TokenInfo)) + end + + def swagger_definitions do + %{ + TokenInfo: + swagger_schema do + title("ChatKit Token") + description("Token used for connection to ChatKit's server") + + properties do + access_token(:string, "Bearer token", required: true) + + expires_in(:string, "TTL of token", required: true) + end + end + } + end +end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 0b7ef1a0d..6ecd6d5e6 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -39,6 +39,8 @@ defmodule CadetWeb.Router do post("/grading/:submissionid/:questionid", GradingController, :update) get("/user", UserController, :index) + + post("/chat/token", ChatController, :index) end # Other scopes may use custom stacks. diff --git a/lib/cadet_web/views/chat_view.ex b/lib/cadet_web/views/chat_view.ex new file mode 100644 index 000000000..1262ef865 --- /dev/null +++ b/lib/cadet_web/views/chat_view.ex @@ -0,0 +1,10 @@ +defmodule CadetWeb.ChatView do + use CadetWeb, :view + + def render("index.json", %{access_token: token, expires_in: ttl}) do + %{ + access_token: token, + expires_in: ttl + } + end +end diff --git a/lib/mix/tasks/chatkit_room.ex b/lib/mix/tasks/chatkit_room.ex new file mode 100644 index 000000000..e9507cc46 --- /dev/null +++ b/lib/mix/tasks/chatkit_room.ex @@ -0,0 +1,34 @@ +defmodule Mix.Tasks.Cadet.ChatkitRoom do + @moduledoc """ + Creates ChatKit rooms for answers with empty comments in the database. + Room creation: https://pusher.com/docs/chatkit/reference/api#create-a-room + Status codes: https://pusher.com/docs/chatkit/reference/api#response-and-error-codes + + Note: + - Task is to run daily + """ + use Mix.Task + + import Ecto.Query + import Mix.EctoSQL + + alias Cadet.Repo + alias Cadet.Assessments.Submission + alias Cadet.Chat.Room + + def run(_args) do + ensure_started(Repo, []) + + Submission + |> join(:inner, [s], a in assoc(s, :answers)) + |> join(:inner, [s], u in assoc(s, :student)) + |> preload([_, a, u], answers: a, student: u) + |> where([_, a], is_nil(a.comment)) + |> Repo.all() + |> Enum.each(fn submission -> + Enum.each(submission.answers, fn answer -> + Room.create_rooms(submission, answer, submission.student) + end) + end) + end +end diff --git a/lib/mix/tasks/users/chat.ex b/lib/mix/tasks/users/chat.ex new file mode 100644 index 000000000..d99e56a27 --- /dev/null +++ b/lib/mix/tasks/users/chat.ex @@ -0,0 +1,53 @@ +defmodule Mix.Tasks.Cadet.Users.Chat do + @moduledoc """ + Creates ChatKit accounts for all users in the database. + User creation: https://pusher.com/docs/chatkit/reference/api#create-a-user + Status codes: https://pusher.com/docs/chatkit/reference/api#response-and-error-codes + + Note: + - Task is to run after `import` (i.e. db is populated) + - user_id from User is used as the unique identifier for Chatkit + + Assumption + - User with the id "admin" already exist in the ChatKit instance. + """ + use Mix.Task + + require Logger + + import Mix.EctoSQL + + alias Cadet.Repo + alias Cadet.Accounts.User + alias Cadet.Chat.Token + + @instance_id :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:instance_id) + + def run(_args) do + ensure_started(Repo, []) + HTTPoison.start() + + url = "https://us1.pusherplatform.io/services/chatkit/v4/#{@instance_id}/users" + + {:ok, token} = Token.get_superuser_token() + headers = [Authorization: "Bearer #{token}"] + + User + |> Repo.all() + |> Enum.each(fn user -> + body = Poison.encode!(%{"name" => user.name, "id" => to_string(user.id)}) + + case HTTPoison.post(url, body, headers) do + {:ok, %HTTPoison.Response{status_code: 201}} -> + :ok + + {:ok, %HTTPoison.Response{body: body}} -> + Logger.error("Unable to create user (name: #{user.name}, user_id: #{user.id})") + Logger.error("error: #{Poison.decode!(body)["error_description"]}") + + {:error, %HTTPoison.Error{reason: error}} -> + Logger.error("error: #{inspect(error, pretty: true)}") + end + end) + end +end diff --git a/mix.exs b/mix.exs index 8fd22c035..9d295d5cb 100644 --- a/mix.exs +++ b/mix.exs @@ -57,6 +57,7 @@ defmodule Cadet.Mixfile do {:guardian_db, "~> 2.0"}, {:httpoison, "~> 1.0", override: true}, {:inch_ex, "~> 2.0", only: [:dev, :test]}, + {:joken, "~> 2.0"}, {:jason, "~> 1.1"}, {:jsx, "~> 2.8"}, {:phoenix, "~> 1.4.0"}, diff --git a/mix.lock b/mix.lock index d00c7a44f..5dec88fc4 100644 --- a/mix.lock +++ b/mix.lock @@ -13,8 +13,8 @@ "credo": {:hex, :credo, "1.1.0", "e0c07b2fd7e2109495f582430a1bc96b2c71b7d94c59dfad120529f65f19872f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm"}, - "db_connection": {:hex, :db_connection, "2.1.0", "122e2f62c4906bf2e49554f1e64db5030c19229aa40935f33088e7d543aa79d0", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [], [], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.1.0", "122e2f62c4906bf2e49554f1e64db5030c19229aa40935f33088e7d543aa79d0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, "distillery": {:hex, :distillery, "2.1.0", "5f31c7771923c12dbb79dcd8d01c5913b07222f134c327e7ab026acdabae985b", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, "ecto": {:hex, :ecto, "3.0.9", "f01922a0b91a41d764d4e3a914d7f058d99a03460d3082c61dd2dcadd724c934", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, @@ -45,6 +45,7 @@ "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "joken": {:hex, :joken, "2.1.0", "bf21a73105d82649f617c5e59a7f8919aa47013d2519ebcc39d998d8d12adda9", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, diff --git a/test/cadet/chat/room_test.exs b/test/cadet/chat/room_test.exs new file mode 100644 index 000000000..6cfa0cfa8 --- /dev/null +++ b/test/cadet/chat/room_test.exs @@ -0,0 +1,151 @@ +defmodule Cadet.Chat.RoomTest do + @moduledoc """ + All tests in this module use pre-recorded HTTP responses saved by ExVCR. + this allows testing without the use of actual external Chatkit API calls. + """ + + use Cadet.DataCase + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + import Ecto.Query + import ExUnit.CaptureLog + + alias Cadet.Assessments.Submission + alias Cadet.Chat.Room + alias Cadet.Repo + + setup do + student = insert(:user, %{role: :student}) + assessment = insert(:assessment) + submission = insert(:submission, %{student: student, assessment: assessment}) + question = insert(:question, %{assessment: assessment}) + + answer_no_comment = + insert(:answer, %{ + submission_id: submission.id, + question_id: question.id, + comment: nil + }) + + {:ok, + %{ + student: student, + answer_no_comment: answer_no_comment, + assessment: assessment, + submission: submission + }} + end + + describe "create a room on chatkit if answer does not have a comment" do + test "success", %{ + answer_no_comment: answer, + assessment: assessment, + submission: submission, + student: student + } do + use_cassette "chatkit/room#1" do + Room.create_rooms(submission, answer, student) + + answer_db = + Submission + |> where(assessment_id: ^assessment.id) + |> where(student_id: ^student.id) + |> join(:inner, [s], a in assoc(s, :answers)) + |> select([_, a], a) + |> Repo.one() + + assert answer_db.comment == "19420137" + end + end + + test "user does not exist", %{ + answer_no_comment: answer, + assessment: assessment, + submission: submission, + student: student + } do + use_cassette "chatkit/room#2" do + error = "services/chatkit/bad_request/users_not_found" + error_description = "1 out of 2 users (including the room creator) could not be found" + status_code = 400 + + assert capture_log(fn -> + Room.create_rooms(submission, answer, student) + + answer_db = + Submission + |> where(assessment_id: ^assessment.id) + |> where(student_id: ^student.id) + |> join(:inner, [s], a in assoc(s, :answers)) + |> select([_, a], a) + |> Repo.one() + + assert answer_db == answer + end) =~ + "Room creation failed: #{error}, #{error_description} (status code #{status_code}) " <> + "[user_id: #{student.id}, assessment_id: #{assessment.id}, question_id: #{ + answer.question_id + }]" + end + end + + test "user does not have permission", %{ + answer_no_comment: answer, + assessment: assessment, + submission: submission, + student: student + } do + use_cassette "chatkit/room#3" do + error = "services/chatkit_authorizer/authorization/missing_permission" + error_description = "User does not have access to requested resource" + status_code = 401 + + assert capture_log(fn -> + Room.create_rooms(submission, answer, student) + + answer_db = + Submission + |> where(assessment_id: ^assessment.id) + |> where(student_id: ^student.id) + |> join(:inner, [s], a in assoc(s, :answers)) + |> select([_, a], a) + |> Repo.one() + + assert answer_db == answer + end) =~ + "Room creation failed: #{error}, #{error_description} (status code #{status_code}) " <> + "[user_id: #{student.id}, assessment_id: #{assessment.id}, question_id: #{ + answer.question_id + }]" + end + end + end + + describe "do not create a room on chatkit if answer has a comment" do + test "success", %{ + student: student + } do + assessment = insert(:assessment) + submission = insert(:submission, %{student: student, assessment: assessment}) + question = insert(:question, %{assessment: assessment}) + + answer_with_comment = + insert(:answer, %{ + submission_id: submission.id, + question_id: question.id + }) + + Room.create_rooms(submission, answer_with_comment, student) + + answer_db = + Submission + |> where(assessment_id: ^assessment.id) + |> where(student_id: ^student.id) + |> join(:inner, [s], a in assoc(s, :answers)) + |> select([_, a], a) + |> Repo.one() + + assert answer_db.comment == answer_with_comment.comment + end + end +end diff --git a/test/cadet/jobs/autograder/grading_job_test.exs b/test/cadet/jobs/autograder/grading_job_test.exs index 0ee816bf8..25c0d4869 100644 --- a/test/cadet/jobs/autograder/grading_job_test.exs +++ b/test/cadet/jobs/autograder/grading_job_test.exs @@ -250,7 +250,7 @@ defmodule Cadet.Autograder.GradingJobTest do assert answer.grade == 0 assert answer.autograding_status == :success assert answer.answer == %{"code" => "// Question not answered by student."} - assert answer.comment == "Question not attempted by student" + assert answer.comment == nil end assert Enum.empty?(JobsQueue.all()) @@ -314,7 +314,7 @@ defmodule Cadet.Autograder.GradingJobTest do assert answer.xp == 0 assert answer.autograding_status == :success assert answer.answer == %{"code" => "// Question not answered by student."} - assert answer.comment == "Question not attempted by student" + assert answer.comment == nil end end end @@ -365,7 +365,7 @@ defmodule Cadet.Autograder.GradingJobTest do assert answer.xp == 0 assert answer.autograding_status == :success assert answer.answer == %{"choice_id" => 0} - assert answer.comment == "Question not attempted by student" + assert answer.comment == nil end assert Enum.empty?(JobsQueue.all()) diff --git a/test/cadet_web/controllers/answer_controller_test.exs b/test/cadet_web/controllers/answer_controller_test.exs index 9280ea590..31cc09612 100644 --- a/test/cadet_web/controllers/answer_controller_test.exs +++ b/test/cadet_web/controllers/answer_controller_test.exs @@ -2,6 +2,7 @@ defmodule CadetWeb.AnswerControllerTest do use CadetWeb.ConnCase import Ecto.Query + import Mock alias Cadet.Assessments.{Answer, Submission} alias Cadet.Repo @@ -24,6 +25,12 @@ defmodule CadetWeb.AnswerControllerTest do } end + setup_with_mocks([ + {Cadet.Chat.Room, [], [create_rooms: fn _submission, _answer, _student -> nil end]} + ]) do + :ok + end + describe "POST /assessments/question/{questionId}/submit/, Unauthenticated" do test "is disallowed", %{conn: conn, mcq_question: question} do conn = post(conn, build_url(question.id), %{answer: 5}) diff --git a/test/cadet_web/controllers/grading_controller_test.exs b/test/cadet_web/controllers/grading_controller_test.exs index 62d1bd7a8..0f9267b3a 100644 --- a/test/cadet_web/controllers/grading_controller_test.exs +++ b/test/cadet_web/controllers/grading_controller_test.exs @@ -497,7 +497,7 @@ defmodule CadetWeb.GradingControllerTest do assert %{ adjustment: -10, - comment: "Never gonna give you up", + comment: comment, xp_adjustment: -10, grader_id: ^grader_id } = Repo.get(Answer, answer.id) @@ -557,7 +557,7 @@ defmodule CadetWeb.GradingControllerTest do assert %{ adjustment: -100, - comment: "Your awesome", + comment: comment, xp_adjustment: -100, grader_id: ^mentor_id } = Repo.get(Answer, answer.id) @@ -610,7 +610,8 @@ defmodule CadetWeb.GradingControllerTest do assert submission_db.unsubmitted_by_id === grader.id assert submission_db.unsubmitted_at != nil - assert answer_db.comment == nil + # Chatkit roomid should not be removed when a submission is unsubmitted + assert answer_db.comment == answer.comment assert answer_db.autograding_status == :none assert answer_db.autograding_results == [] assert answer_db.grader_id == nil @@ -769,7 +770,8 @@ defmodule CadetWeb.GradingControllerTest do assert submission_db.unsubmitted_by_id === admin.id assert submission_db.unsubmitted_at != nil - assert answer_db.comment == nil + # Chatkit roomid should not be removed when a submission is unsubmitted + assert answer_db.comment == answer.comment assert answer_db.autograding_status == :none assert answer_db.autograding_results == [] assert answer_db.grader_id == nil @@ -1009,7 +1011,7 @@ defmodule CadetWeb.GradingControllerTest do }) assert response(conn, 200) == "OK" - assert %{adjustment: -10, comment: "Never gonna give you up"} = Repo.get(Answer, answer.id) + assert %{adjustment: -10, comment: comment} = Repo.get(Answer, answer.id) end @tag authenticate: :admin diff --git a/test/fixtures/vcr_cassettes/chatkit/room#1.json b/test/fixtures/vcr_cassettes/chatkit/room#1.json new file mode 100644 index 000000000..0e967a450 --- /dev/null +++ b/test/fixtures/vcr_cassettes/chatkit/room#1.json @@ -0,0 +1,28 @@ +[ + { + "request": { + "body": "", + "headers": {}, + "method": "post", + "options": [], + "request_body": "", + "url": "https://us1.pusherplatform.io/services/chatkit/v4/instance_id/rooms" + }, + "response": { + "binary": false, + "body": "{\"created_at\":\"2019-07-06T09:00:47Z\",\"created_by_id\":\"admin\",\"id\":\"19420137\",\"member_user_ids\":[\"admin\",\"1\",\"2\",\"3\"],\"name\":\"room_name\",\"private\":true,\"updated_at\":\"2019-07-06T09:00:47Z\"}", + "headers": { + "access-control-expose-headers": "Server, Access-Control-Expose-Headers, X-Request-Id, Date, X-Envoy-Upstream-Service-Time, Access-Control-Max-Age", + "access-control-max-age": "86400", + "content-type": "application/json", + "date": "Sat, 06 Jul 2019 09:00:47 GMT", + "server": "istio-envoy", + "x-envoy-upstream-service-time": "44", + "x-request-id": "f61cc0eb-2261-4817-a8a6-1637c5202a96", + "content-length": "187" + }, + "status_code": 201, + "type": "ok" + } + } +] diff --git a/test/fixtures/vcr_cassettes/chatkit/room#2.json b/test/fixtures/vcr_cassettes/chatkit/room#2.json new file mode 100644 index 000000000..e0b9be443 --- /dev/null +++ b/test/fixtures/vcr_cassettes/chatkit/room#2.json @@ -0,0 +1,27 @@ +[ + { + "request": { + "body": "", + "headers": {}, + "method": "post", + "options": [], + "request_body": "", + "url": "https://us1.pusherplatform.io/services/chatkit/v4/instance_id/rooms" + }, + "response": { + "binary": false, + "body": "{\"error\":\"services/chatkit/bad_request/users_not_found\",\"error_description\":\"1 out of 2 users (including the room creator) could not be found\",\"error_uri\":\"https://docs.pusher.com/errors/services/chatkit/bad_request/users_not_found\"}", + "headers": { + "access-control-expose-headers": "Access-Control-Max-Age, Date, X-Envoy-Upstream-Service-Time, Server, Access-Control-Expose-Headers", + "access-control-max-age": "86400", + "content-type": "application/json", + "date": "Sat, 06 Jul 2019 10:23:00 GMT", + "server": "istio-envoy", + "x-envoy-upstream-service-time": "11", + "content-length": "233" + }, + "status_code": 400, + "type": "ok" + } + } +] diff --git a/test/fixtures/vcr_cassettes/chatkit/room#3.json b/test/fixtures/vcr_cassettes/chatkit/room#3.json new file mode 100644 index 000000000..b5a304d9b --- /dev/null +++ b/test/fixtures/vcr_cassettes/chatkit/room#3.json @@ -0,0 +1,27 @@ +[ + { + "request": { + "body": "", + "headers": {}, + "method": "post", + "options": [], + "request_body": "", + "url": "https://us1.pusherplatform.io/services/chatkit/v4/instance_id/rooms" + }, + "response": { + "binary": false, + "body": "{\"error\":\"services/chatkit_authorizer/authorization/missing_permission\",\"error_description\":\"User does not have access to requested resource\",\"error_uri\":\"https://docs.pusher.com/errors/services/chatkit_authorizer/authorization/missing_permission\"}", + "headers": { + "access-control-expose-headers": "Access-Control-Max-Age", + "access-control-max-age": "86400", + "content-type": "application/json", + "content-length": "248", + "date": "Sat, 06 Jul 2019 11:17:23 GMT", + "x-envoy-upstream-service-time": "13", + "server": "istio-envoy" + }, + "status_code": 401, + "type": "ok" + } + } +]