Fix ML-143 review: security, worker, and test improvements

Closes ML-143 review findings:
- cast_id/1 uses Integer.parse instead of String.to_integer
- add_to_cart validates against server-side release_groups
- JS.push payload trimmed to {id, format} only
- Oban.insert_all result checked for partial failures
- handle_async success resets @importing? to false
- ImportFromMusicbrainzReleaseGroup gets unique constraint
- Record.parse_datetime/1 made public with safe match
- Workers delegate to Record.parse_datetime/1
- Test coverage: change_format, clear_cart, wishlist single import
This commit is contained in:
Claudio Ortolina
2026-04-20 13:03:38 +01:00
parent 3f5ce060b1
commit c4ebe636ff
9 changed files with 228 additions and 66 deletions
@@ -1,10 +1,11 @@
defmodule MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroupTest do
use MusicLibrary.DataCase
use MusicLibrary.DataCase, async: true
import MusicBrainz.Fixtures.ReleaseGroup
import MusicLibrary.Fixtures.Records
alias MusicLibrary.Records.Record
alias MusicLibrary.Worker.FetchArtistInfo
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
describe "perform/1" do
@@ -41,6 +42,11 @@ defmodule MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroupTest do
assert imported_record.title == "Marbles"
assert imported_record.format == :cd
assert imported_record.purchased_at == DateTime.truncate(purchased_at, :second)
assert_enqueued(
worker: FetchArtistInfo,
args: %{"id" => "1932f5b6-0b7b-4050-b1df-833ca89e5f44"}
)
end
test "imports a wishlist record when purchased_at is nil" do
@@ -348,6 +348,82 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
assert_has(session, "#cart-empty")
end
test "changes the format of a cart item", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_release_group_search()
[first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import")
view
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("#cart-items form")
|> LVT.render_change(%{"format" => "vinyl"})
assert LVT.render(view) =~ "In cart · Vinyl"
end
test "rejects change_format when the resulting pair is already in the cart", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_release_group_search()
[first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import")
view
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("#musicbrainz_#{first_id} a", "Vinyl")
|> LVT.render_click()
html = LVT.render(view)
cart_item_ids = Regex.scan(~r/id="cart-item-(\d+)"/, html, capture: :all_but_first)
assert length(cart_item_ids) == 2
[first_id_match | _] = cart_item_ids
[item_id] = first_id_match
view
|> LVT.element("#cart-item-#{item_id} form")
|> LVT.render_change(%{"cart_item_id" => item_id, "format" => "vinyl"})
html_after = LVT.render(view)
assert Regex.scan(~r/id="cart-item-/, html_after) |> length() == 2
end
test "clears the cart", %{conn: conn} do
stub_release_group_search()
[first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
conn
|> visit(~p"/collection/import")
|> fill_in("Search for a record", with: "Marillion Marbles")
|> click_link("#musicbrainz_#{first_id} a", "CD")
|> click_button("Clear all")
|> assert_has("#cart-empty")
end
test "imports a single cart item synchronously and navigates", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
@@ -378,7 +454,7 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
assert record.musicbrainz_id == first_id
assert record.title == "Marbles"
assert record.format == :cd
assert record.purchased_at != nil
refute is_nil(record.purchased_at)
{:ok, resized_cover_data} = Image.resize(marbles_cover_data())
assets = Assets.get(record.cover_hash)
@@ -2,8 +2,10 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
use MusicLibraryWeb.ConnCase
use Oban.Testing, repo: MusicLibrary.BackgroundRepo
import Ecto.Query, only: [from: 2]
import MusicBrainz.Fixtures.ReleaseGroup
import MusicLibrary.Fixtures.Records
import Phoenix.LiveViewTest, only: [assert_redirect: 2]
alias MusicLibrary.Records.Record
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
@@ -29,18 +31,13 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
purchased_record = MusicLibrary.Records.get_record!(record.id)
assert purchased_record.purchased_at !== nil
refute is_nil(purchased_record.purchased_at)
end
end
describe "Adding a new record" do
test "enqueues one job per cart item with purchased_at: nil for wishlist", %{conn: conn} do
Req.Test.stub(MusicBrainz.API, fn conn ->
case conn.path_info do
[_ws, _version, "release-group"] ->
Req.Test.json(conn, release_group_search_results())
end
end)
stub_release_group_search()
[first, second | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
@@ -72,9 +69,77 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
}
)
import Ecto.Query, only: [from: 2]
query = from(r in Record, where: not is_nil(r.purchased_at))
assert MusicLibrary.Repo.all(query) == []
end
test "imports a single cart item synchronously and navigates to wishlist", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_full_import()
[first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/wishlist/import")
view
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("button", "Import 1 record")
|> LVT.render_click()
{path, _flash} = assert_redirect(view, 2_000)
"/wishlist/" <> record_id = path
record = MusicLibrary.Records.get_record!(record_id)
assert record.musicbrainz_id == first_id
assert record.title == "Marbles"
assert record.format == :cd
assert record.purchased_at == nil
refute_enqueued(worker: ImportFromMusicbrainzReleaseGroup)
end
end
defp stub_release_group_search do
Req.Test.stub(MusicBrainz.API, fn conn ->
case conn.path_info do
[_ws, _version, "release-group"] ->
Req.Test.json(conn, release_group_search_results())
end
end)
end
defp stub_full_import do
[first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"]
release_group = release_group(:marbles)
release_group_releases = release_group_releases(:marbles)
cover_data = marbles_cover_data()
Req.Test.stub(MusicBrainz.API, fn conn ->
case conn.path_info do
[_ws, _version, "release-group", ^first_id] ->
Req.Test.json(conn, release_group)
[_ws, _version, "release-group"] ->
Req.Test.json(conn, release_group_search_results())
[_ws, _version, "release"] ->
Req.Test.json(conn, release_group_releases)
[_release_group, ^first_id, "front"] ->
Plug.Conn.send_resp(conn, 200, cover_data)
end
end)
end
end