diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8fdf30..07ddf29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,7 +200,7 @@ jobs: uses: josecfreittas/elixir-coverage-feedback-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - coverage_threshold: 70 + coverage_threshold: 80 services: pg: image: postgres:16 diff --git a/apps/core/lib/core/gcp/manager.ex b/apps/core/lib/core/gcp/manager.ex index 4875a00..c182772 100644 --- a/apps/core/lib/core/gcp/manager.ex +++ b/apps/core/lib/core/gcp/manager.ex @@ -51,7 +51,6 @@ defmodule GoEscuelaLms.Core.GCP.Manager do def connection() do {:ok, token} = Goth.fetch(Core.Goth) - IO.puts("connection ==> #{inspect(token)}") GoogleApi.Storage.V1.Connection.new(token.token) end diff --git a/apps/core/lib/core/schema/question.ex b/apps/core/lib/core/schema/question.ex index 5c4e14b..47ab79e 100644 --- a/apps/core/lib/core/schema/question.ex +++ b/apps/core/lib/core/schema/question.ex @@ -41,8 +41,6 @@ defmodule GoEscuelaLms.Core.Schema.Question do end def bulk_create(activity, records) do - IO.puts("RECORDS ===> #{inspect(records)}") - Repo.transaction(fn -> Enum.each(records, fn record -> question = diff --git a/apps/core/lib/core/schema/topic.ex b/apps/core/lib/core/schema/topic.ex index 157cd90..9353e09 100644 --- a/apps/core/lib/core/schema/topic.ex +++ b/apps/core/lib/core/schema/topic.ex @@ -23,6 +23,7 @@ defmodule GoEscuelaLms.Core.Schema.Topic do def find(uuid) do Repo.get(Topic, uuid) + |> Repo.preload(:course) |> Repo.preload(:activities) end diff --git a/apps/core/test/core/schema/topic_test.exs b/apps/core/test/core/schema/topic_test.exs index 411737f..84aad30 100644 --- a/apps/core/test/core/schema/topic_test.exs +++ b/apps/core/test/core/schema/topic_test.exs @@ -33,7 +33,10 @@ defmodule Core.TopicTest do describe "find/1" do test "when exist", %{course: course} do - topic = insert!(:topic, course_id: course.uuid) |> Repo.preload(:activities) + topic = + insert!(:topic, course_id: course.uuid) + |> Repo.preload(:course) + |> Repo.preload(:activities) assert Topic.find(topic.uuid) == topic end diff --git a/apps/web/lib/web/controllers/activities/activities_controller.ex b/apps/web/lib/web/controllers/activities/activities_controller.ex index e26339e..f1c6e30 100644 --- a/apps/web/lib/web/controllers/activities/activities_controller.ex +++ b/apps/web/lib/web/controllers/activities/activities_controller.ex @@ -59,7 +59,7 @@ defmodule Web.Activities.ActivitiesController do } def create(conn, params) do - topic = conn.assigns.topic + topic = conn.assigns.object with {:ok, valid_params} <- Tarams.cast(params, @create_params), {:ok, valid_params} <- activity_type_valid_params(params, valid_params), diff --git a/apps/web/lib/web/controllers/topics/topics_controller.ex b/apps/web/lib/web/controllers/topics/topics_controller.ex index 5bb69fd..10dfd32 100644 --- a/apps/web/lib/web/controllers/topics/topics_controller.ex +++ b/apps/web/lib/web/controllers/topics/topics_controller.ex @@ -8,10 +8,10 @@ defmodule Web.Topics.TopicsController do alias GoEscuelaLms.Core.Schema.Topic - plug :permit_authorized when action in [:create] + plug :permit_authorized when action in [:create, :update, :delete] plug :load_course when action in [:create] - plug :check_enrollment when action in [:create] plug :load_topic when action in [:update, :delete] + plug :check_enrollment when action in [:create, :update, :delete] @create_params %{ name: [type: :string, required: true] @@ -27,7 +27,7 @@ defmodule Web.Topics.TopicsController do end def update(conn, params) do - topic = conn.assigns.topic + topic = conn.assigns.object with {:ok, valid_params} <- Tarams.cast(params, @create_params), {:ok, topic_updated} <- update_topic(topic, valid_params) do @@ -36,7 +36,7 @@ defmodule Web.Topics.TopicsController do end def delete(conn, _params) do - topic = conn.assigns.topic + topic = conn.assigns.object case topic |> Topic.delete() do {:ok, topic} -> diff --git a/apps/web/lib/web/controllers/users/users_controller.ex b/apps/web/lib/web/controllers/users/users_controller.ex index 9937ece..85f632f 100644 --- a/apps/web/lib/web/controllers/users/users_controller.ex +++ b/apps/web/lib/web/controllers/users/users_controller.ex @@ -8,7 +8,7 @@ defmodule Web.Users.UsersController do alias GoEscuelaLms.Core.Schema.User - plug :organizer_authorized when action in [:create] + plug :organizer_authorized when action in [:create, :index] plug :load_user when action in [:update, :delete] @create_params %{ diff --git a/apps/web/lib/web/controllers/users/users_json.ex b/apps/web/lib/web/controllers/users/users_json.ex index 1568e7d..bbd0276 100644 --- a/apps/web/lib/web/controllers/users/users_json.ex +++ b/apps/web/lib/web/controllers/users/users_json.ex @@ -25,7 +25,9 @@ defmodule Web.Users.UsersJSON do %{ id: user.uuid, email: user.email, - full_name: user.full_name + role: user.role, + full_name: user.full_name, + inserted_at: user.inserted_at |> to_string() } end end diff --git a/apps/web/lib/web/plug/check_request.ex b/apps/web/lib/web/plug/check_request.ex index 632f638..3ee5bf3 100644 --- a/apps/web/lib/web/plug/check_request.ex +++ b/apps/web/lib/web/plug/check_request.ex @@ -38,7 +38,7 @@ defmodule Web.Plug.CheckRequest do with :ok <- valid_uuids(id), object <- Topic.find(id), false <- is_nil(object) do - assign(conn, :topic, object) + assign(conn, :object, object) else _ -> Web.FallbackController.call(conn, {:error, "topic is invalid"}) |> halt() @@ -61,12 +61,14 @@ defmodule Web.Plug.CheckRequest do def check_enrollment(%{assigns: %{account: %{role: :organizer}}} = conn, _), do: conn def check_enrollment(conn, _) do - user_id = conn.assigns.account.uuid - course = conn.assigns.course + account = conn.assigns.account + enrollments = account.enrollments + + course = conn.assigns |> get_in([:course]) || conn.assigns.object.course case is_nil( - course.enrollments - |> Enum.find(fn enrollment -> enrollment.user_id == user_id end) + enrollments + |> Enum.find(fn enrollment -> enrollment.course_id == course.uuid end) ) do false -> conn diff --git a/apps/web/lib/web/router.ex b/apps/web/lib/web/router.ex index f235d30..e3214bc 100644 --- a/apps/web/lib/web/router.ex +++ b/apps/web/lib/web/router.ex @@ -28,13 +28,14 @@ defmodule Web.Router do end resources "/courses", Courses.CoursesController, only: [:create, :update, :index, :show] do - resources "/topics", Topics.TopicsController, only: [:create, :update, :delete] do + resources "/topics", Topics.TopicsController, only: [:create] do resources "/activities", Activities.ActivitiesController, only: [:create] end resources "/enrollments", Enrollments.EnrollmentsController, only: [:index] end + resources "/topics", Topics.TopicsController, only: [:update, :delete] resources "/enrollments", Enrollments.EnrollmentsController, only: [:index, :create, :delete] get "/profile", Users.ProfileController, :show diff --git a/apps/web/mix.exs b/apps/web/mix.exs index ca15316..9d3131d 100644 --- a/apps/web/mix.exs +++ b/apps/web/mix.exs @@ -16,7 +16,7 @@ defmodule Web.MixProject do deps: deps(), test_coverage: [ summary: [ - threshold: 70 + threshold: 80 ] ] ] diff --git a/apps/web/test/web/controllers/topics_controller_test.exs b/apps/web/test/web/controllers/topics_controller_test.exs new file mode 100644 index 0000000..1cb8103 --- /dev/null +++ b/apps/web/test/web/controllers/topics_controller_test.exs @@ -0,0 +1,232 @@ +defmodule Web.TopicsControllerTest do + use ExUnit.Case + use Core.DataCase + use Web.ConnCase + + import Web.Auth.Guardian + import Core.Factory + + alias GoEscuelaLms.Core.Schema.Topic + + setup do + course = insert!(:course) + student = insert!(:user, role: :student) + instructor = insert!(:user, role: :instructor) + organizer = insert!(:user, role: :organizer) + + {:ok, student: student, instructor: instructor, organizer: organizer, course: course} + end + + describe "create/2" do + test "unauthorized", %{student: student, course: course} do + {:ok, token, _} = encode_and_sign(student, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, student.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/courses/#{course.uuid}/topics", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid course", %{organizer: organizer} do + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/courses/#{Faker.UUID.v4()}/topics", %{}) + + assert json_response(conn, 422)["errors"] == %{"detail" => "course is invalid"} + end + + test "invalid enrollment", %{instructor: instructor, course: course} do + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/courses/#{course.uuid}/topics", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid params", %{instructor: instructor, course: course} do + insert!(:enrollment, course_id: course.uuid, user_id: instructor.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/courses/#{course.uuid}/topics", %{name: nil}) + + assert json_response(conn, 422)["errors"] == %{"detail" => %{"name" => ["is required"]}} + end + + test "valid params", %{instructor: instructor, course: course} do + insert!(:enrollment, course_id: course.uuid, user_id: instructor.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + name = Faker.Lorem.word() + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/courses/#{course.uuid}/topics", %{name: name}) + + response = json_response(conn, 200)["data"] + + assert response["name"] == name + assert is_nil(response["id"]) == false + end + end + + describe "update/2" do + test "unauthorized", %{student: student, course: course} do + topic = insert!(:topic, course_id: course.uuid) + + {:ok, token, _} = encode_and_sign(student, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, student.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> put(~p"/api/topics/#{topic.uuid}", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid topic", %{organizer: organizer} do + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> put(~p"/api/topics/#{Faker.UUID.v4()}", %{}) + + assert json_response(conn, 422)["errors"] == %{"detail" => "topic is invalid"} + end + + test "invalid enrollment", %{instructor: instructor, course: course} do + topic = insert!(:topic, course_id: course.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> put(~p"/api/topics/#{topic.uuid}", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid params", %{instructor: instructor, course: course} do + topic = insert!(:topic, course_id: course.uuid) + insert!(:enrollment, course_id: course.uuid, user_id: instructor.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> put(~p"/api/topics/#{topic.uuid}", %{name: nil}) + + assert json_response(conn, 422)["errors"] == %{"detail" => %{"name" => ["is required"]}} + end + + test "valid params", %{instructor: instructor, course: course} do + topic = insert!(:topic, course_id: course.uuid) + insert!(:enrollment, course_id: course.uuid, user_id: instructor.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + name = Faker.Lorem.word() + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> put(~p"/api/topics/#{topic.uuid}", %{name: name}) + + response = json_response(conn, 200)["data"] + + assert response["name"] == name + assert is_nil(response["id"]) == false + end + end + + describe "delete/2" do + test "unauthorized", %{student: student, course: course} do + topic = insert!(:topic, course_id: course.uuid) + + {:ok, token, _} = encode_and_sign(student, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, student.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> delete(~p"/api/topics/#{topic.uuid}", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid topic", %{organizer: organizer} do + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> delete(~p"/api/topics/#{Faker.UUID.v4()}", %{}) + + assert json_response(conn, 422)["errors"] == %{"detail" => "topic is invalid"} + end + + test "invalid enrollment", %{instructor: instructor, course: course} do + topic = insert!(:topic, course_id: course.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> delete(~p"/api/topics/#{topic.uuid}", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "valid delete", %{instructor: instructor, course: course} do + topic = insert!(:topic, course_id: course.uuid) + insert!(:enrollment, course_id: course.uuid, user_id: instructor.uuid) + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> delete(~p"/api/topics/#{topic.uuid}") + + assert json_response(conn, 200) + assert Topic.find(topic.uuid) == nil + end + end +end diff --git a/apps/web/test/web/controllers/users_controller_test.exs b/apps/web/test/web/controllers/users_controller_test.exs new file mode 100644 index 0000000..92097c1 --- /dev/null +++ b/apps/web/test/web/controllers/users_controller_test.exs @@ -0,0 +1,126 @@ +defmodule Web.UsersControllerTest do + use ExUnit.Case + use Core.DataCase + use Web.ConnCase + + import Web.Auth.Guardian + import Core.Factory + + setup do + instructor = insert!(:user, role: :instructor) + organizer = insert!(:user, role: :organizer) + + {:ok, instructor: instructor, organizer: organizer} + end + + describe "create/2" do + test "unauthorized", %{instructor: instructor} do + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/users", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "invalid params", %{organizer: organizer} do + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/users", %{}) + + assert json_response(conn, 422)["errors"] == %{ + "detail" => %{ + "email" => ["is required"], + "full_name" => ["is required"], + "password" => ["is required"], + "role" => ["is required"] + } + } + end + + test "valid params", %{organizer: organizer} do + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + user = + build(:user) + |> Map.from_struct() + |> Map.merge(%{role: "student", password: Faker.String.base64(100)}) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> post(~p"/api/users", user) + + response = json_response(conn, 200)["data"] + + assert response["full_name"] == user.full_name + assert response["email"] == user.email + assert response["role"] == user.role + end + end + + describe "index/2" do + test "unauthorized", %{instructor: instructor} do + {:ok, token, _} = encode_and_sign(instructor, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, instructor.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> get(~p"/api/users", %{}) + + assert json_response(conn, 403)["errors"] == %{"detail" => "Forbidden resource"} + end + + test "get all users", %{instructor: instructor, organizer: organizer} do + student = insert!(:user, role: :student) + + {:ok, token, _} = encode_and_sign(organizer, %{}, token_type: :access) + + conn = + session_conn() + |> put_session(:user_id, organizer.uuid) + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> token) + |> get(~p"/api/users") + + response = json_response(conn, 200)["data"] + + assert response == [ + %{ + "id" => instructor.uuid, + "full_name" => instructor.full_name, + "email" => instructor.email, + "role" => instructor.role |> to_string(), + "inserted_at" => instructor.inserted_at |> to_string() + }, + %{ + "id" => organizer.uuid, + "full_name" => organizer.full_name, + "email" => organizer.email, + "role" => organizer.role |> to_string(), + "inserted_at" => organizer.inserted_at |> to_string() + }, + %{ + "id" => student.uuid, + "full_name" => student.full_name, + "email" => student.email, + "role" => student.role |> to_string(), + "inserted_at" => student.inserted_at |> to_string() + } + ] + end + end +end diff --git a/config/test.exs b/config/test.exs index da9baa6..69577ee 100644 --- a/config/test.exs +++ b/config/test.exs @@ -10,3 +10,5 @@ config :web, Web.Endpoint, config :core, GoEscuelaLms.Core.Repo, url: System.get_env("TEST_DATABASE_URL"), pool: Ecto.Adapters.SQL.Sandbox + +config :logger, level: :error