If you go to our router, you’ll see:
live_session :movies do
live "/movies", MovieLive.Index
live "/movies/:id", MovieLive.Show
end
Our movie uploads are almost identical to album uploads. The key difference is that movie uploads use external uploads (for example, Cloudinary).
Configuring Cloudinary
If you want to follow along in development, create a free Cloudinary account and set:
config :ranger, :cloudinary,
api_key: System.get_env("CLOUDINARY_API_KEY"),
api_secret: System.get_env("CLOUDINARY_API_SECRET"),
cloud_name: System.get_env("CLOUDINARY_CLOUD_NAME")
MovieLive.Index
Two important differences from direct-to-server uploads:
1) allow_upload/3 uses external: &presign_upload/2.
2) In consume_uploaded_entries/3, we generate cloud URLs instead of copying files locally.
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:changeset, Movie.changeset(%{}))
|> allow_upload(:posters,
accept: ~w(.jpg .jpeg .png),
max_entries: 2,
external: &presign_upload/2
)}
end
def handle_event("save", %{"movie" => params}, socket) do
uploaded_file_paths =
consume_uploaded_entries(socket, :posters, fn _, entry ->
{:ok, cloudinary_image_url(entry)}
end)
params
|> Map.put("poster_urls", uploaded_file_paths)
|> Movie.changeset()
|> Repo.insert()
|> case do
{:ok, movie} ->
{:noreply, push_navigate(socket, to: ~p"/movies/#{movie}")}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
presign_upload/2 is where provider metadata gets generated, and cloudinary_image_url/1 is where persisted image URLs are built.
Testing MovieLive
Preview tests remain the same as direct-to-server uploads. The new value here is testing:
- Upload metadata contract
- Stored URL correctness
Test correct metadata
test "generates correct metadata for external upload", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/movies")
{:ok, %{entries: entries}} =
view
|> file_input("#upload-form", :posters, [
%{
name: "fellowship-poster.jpg",
content: File.read!("test/support/images/fellowship-poster.jpg"),
type: "image/jpeg"
}
])
|> preflight_upload()
for {_k, v} <- entries do
assert v.uploader == "Cloudinary"
assert v.url =~ CloudinaryUpload.image_api_url(cloud_name())
assert v.fields[:folder] == "testing-liveview"
assert is_binary(v.fields[:public_id])
refute String.ends_with?(v.fields[:public_id], ".png")
assert is_binary(v.fields[:signature])
refute is_nil(v.fields[:api_key])
refute is_nil(v.fields[:timestamp])
end
end
Test correct image path is saved
test "user can create movie and stores correct URLs", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/movies")
{:ok, show_view, _show_html} =
view
|> upload("fellowship-poster.jpg")
|> create_movie("The Fellowship of the Ring")
|> follow_redirect(conn)
assert has_element?(show_view, "h2", "The Fellowship of the Ring")
assert has_element?(show_view, "[data-role='image']")
assert hd(last_movie().poster_urls) =~ CloudinaryUpload.image_url(cloud_name())
end
defp last_movie do
Movie |> Ecto.Query.last() |> Repo.one()
end
This keeps assertions slightly loose (we don’t assert the exact UUID), while still giving confidence that provider URL generation is correct.
Reminder: LiveView tests do not test JS implementation
No matter what we do here, LiveView tests don’t execute browser-side uploader JavaScript.
If you need full end-to-end confidence including provider integration behavior, consider browser automation (for example, Wallaby), with the caveat that cloud upload testing adds integration complexity.