diff --git a/lib/arc_ecto/schema.ex b/lib/arc_ecto/schema.ex index 515667b..178360e 100644 --- a/lib/arc_ecto/schema.ex +++ b/lib/arc_ecto/schema.ex @@ -5,10 +5,11 @@ defmodule Arc.Ecto.Schema do end end - defmacro cast_attachments(changeset_or_data, params, allowed) do + defmacro cast_attachments(changeset_or_data, params, allowed, options \\ []) do quote bind_quoted: [changeset_or_data: changeset_or_data, params: params, - allowed: allowed] do + allowed: allowed, + options: options] do # If given a changeset, apply the changes to obtain the underlying data scope = case changeset_or_data do @@ -31,9 +32,20 @@ defmodule Arc.Ecto.Schema do params |> Arc.Ecto.Schema.convert_params_to_binary |> Dict.take(allowed) - |> Enum.map(fn - {field, nil} -> {field, nil} - {field, file} -> {field, {file, scope}} + |> Enum.reduce([], fn + # Don't wrap nil casts in the scope object + {field, nil}, fields -> [{field, nil} | fields] + + # Allow casting Plug.Uploads + {field, upload = %{__struct__: Plug.Upload}}, fields -> [{field, {upload, scope}} | fields] + + # If casting a binary (path), ensure we've explicitly allowed paths + {field, path}, fields when is_binary(path) -> + if Keyword.get(options, :allow_paths, false) do + [{field, {path, scope}} | fields] + else + fields + end end) |> Enum.into(%{}) end diff --git a/test/schema_test.exs b/test/schema_test.exs index 3185dc8..86f71a1 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -24,6 +24,13 @@ defmodule ArcTest.Ecto.Schema do |> validate_required(:avatar) end + def path_changeset(user, params \\ :invalid) do + user + |> cast(params, ~w(first_name)a) + |> cast_attachments(params, ~w(avatar)a, allow_paths: true) + |> validate_required(:avatar) + end + def changeset2(user, params \\ :invalid) do user |> cast(params, ~w(first_name)a) @@ -40,6 +47,10 @@ defmodule ArcTest.Ecto.Schema do :ok end + def build_upload(path) do + %{__struct__: Plug.Upload, path: path, file_name: Path.basename(path)} + end + test "supports :invalid changeset" do cs = TestUser.changeset(%TestUser{}) assert cs.valid? == false @@ -47,38 +58,47 @@ defmodule ArcTest.Ecto.Schema do assert cs.errors == [avatar: {"can't be blank", []}] end - test_with_mock "cascades storage success into a valid change", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:ok, "file.png"} end] do - cs = TestUser.changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) - assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) + test_with_mock "cascades storage success into a valid change", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:ok, "file.png"} end] do + upload = build_upload("/path/to/my/file.png") + cs = TestUser.changeset(%TestUser{}, %{"avatar" => upload}) assert cs.valid? %{file_name: "file.png", updated_at: _} = cs.changes.avatar end - test_with_mock "cascades storage error into an error", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:error, :invalid_file} end] do - cs = TestUser.changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) - assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) + test_with_mock "cascades storage error into an error", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:error, :invalid_file} end] do + upload = build_upload("/path/to/my/file.png") + cs = TestUser.changeset(%TestUser{}, %{"avatar" => upload}) + assert called DummyDefinition.store({upload, %TestUser{}}) assert cs.valid? == false assert cs.errors == [avatar: {"is invalid", [type: ArcTest.Ecto.Schema.DummyDefinition.Type]}] end - test_with_mock "converts changeset into schema", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:error, :invalid_file} end] do - TestUser.changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) - assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) + test_with_mock "converts changeset into schema", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:error, :invalid_file} end] do + upload = build_upload("/path/to/my/file.png") + TestUser.changeset(%TestUser{}, %{"avatar" => upload}) + assert called DummyDefinition.store({upload, %TestUser{}}) end - test_with_mock "applies changes to schema", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:error, :invalid_file} end] do - TestUser.changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png", "first_name" => "test"}) - assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{first_name: "test"}}) + test_with_mock "applies changes to schema", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:error, :invalid_file} end] do + upload = build_upload("/path/to/my/file.png") + TestUser.changeset(%TestUser{}, %{"avatar" => upload, "first_name" => "test"}) + assert called DummyDefinition.store({upload, %TestUser{first_name: "test"}}) end - test_with_mock "converts atom keys", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:error, :invalid_file} end] do - TestUser.changeset(%TestUser{}, %{avatar: "/path/to/my/file.png"}) - assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) + test_with_mock "converts atom keys", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:error, :invalid_file} end] do + upload = build_upload("/path/to/my/file.png") + TestUser.changeset(%TestUser{}, %{avatar: upload}) + assert called DummyDefinition.store({upload, %TestUser{}}) end - test_with_mock "casting nil attachments", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:ok, "file.png"} end] do - changeset = TestUser.changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) + test_with_mock "casting nil attachments", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:ok, "file.png"} end] do + changeset = TestUser.changeset(%TestUser{}, %{"avatar" => build_upload("/path/to/my/file.png")}) changeset = TestUser.changeset2(changeset, %{"avatar" => nil}) assert nil == Ecto.Changeset.get_field(changeset, :avatar) end + + test_with_mock "allow_paths => true", DummyDefinition, [store: fn({"/path/to/my/file.png", %TestUser{}}) -> {:ok, "file.png"} end] do + changeset = TestUser.path_changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) + assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) + end end