Improve test coverage

Files Created (8 files, 54 new tests)

  Fixtures

  1. test/support/fixtures/music_library/record_sets.ex — record_set/1
and record_set_with_records/2 helpers
  2. test/support/fixtures/music_library/online_store_templates.ex —
online_store_template/1 helper

  Context Tests

  3. test/music_library/record_sets_test.exs — 20 tests covering search,
count, CRUD, add/remove/move records
  4. test/music_library/online_store_templates_test.exs — 8 tests
covering list, CRUD, URL generation, validation
  5. test/music_library/search_test.exs — 5 tests covering universal
search and search counts
  6. test/music_library/notes_test.exs — 4 tests covering get, create,
update

  LiveView Tests

  7. test/music_library_web/live/record_set_live/index_test.exs — 11
tests covering listing, empty state, search, ordering,
create/edit/delete sets, remove records, reorder records
  8.
test/music_library_web/live/online_store_template_live/index_test.exs —
6 tests covering listing, create/edit/delete, toggle enabled

  Results

  - 297 tests, 0 failures (up from 243 — 54 new tests added)
  - All existing tests continue to pass

There's a remaining gettext warning that needs to be investigated
This commit is contained in:
Claudio Ortolina
2026-02-07 10:39:22 +00:00
parent abb1ab332a
commit 84159a1c0d
8 changed files with 763 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
defmodule MusicLibrary.NotesTest do
use MusicLibrary.DataCase
alias MusicLibrary.Notes
alias MusicLibrary.Notes.Note
@musicbrainz_id Ecto.UUID.generate()
describe "get_note/2" do
test "returns nil when no note exists" do
assert Notes.get_note(:record, Ecto.UUID.generate()) == nil
end
test "returns note for record" do
{:ok, note} =
Notes.create_note(%Note{}, %{
entity: :record,
musicbrainz_id: @musicbrainz_id,
content: "Great album"
})
found = Notes.get_note(:record, @musicbrainz_id)
assert found.id == note.id
assert found.content == "Great album"
end
end
describe "create_note/2" do
test "creates a note" do
assert {:ok, note} =
Notes.create_note(%Note{}, %{
entity: :artist,
musicbrainz_id: @musicbrainz_id,
content: "Fantastic artist"
})
assert note.entity == :artist
assert note.content == "Fantastic artist"
assert note.musicbrainz_id == @musicbrainz_id
end
end
describe "update_note/2" do
test "updates note content" do
{:ok, note} =
Notes.create_note(%Note{}, %{
entity: :record,
musicbrainz_id: @musicbrainz_id,
content: "Original"
})
assert {:ok, updated} = Notes.update_note(note, %{content: "Updated content"})
assert updated.content == "Updated content"
end
end
end
@@ -0,0 +1,95 @@
defmodule MusicLibrary.OnlineStoreTemplatesTest do
use MusicLibrary.DataCase
import MusicLibrary.Fixtures.OnlineStoreTemplates
import MusicLibrary.Fixtures.Records
alias MusicLibrary.OnlineStoreTemplates
describe "list_templates/0 and list_enabled_templates/0" do
test "lists all templates" do
t1 = online_store_template(%{name: "Amazon", enabled: true})
t2 = online_store_template(%{name: "Bandcamp", enabled: false})
all = OnlineStoreTemplates.list_templates()
ids = Enum.map(all, & &1.id)
assert t1.id in ids
assert t2.id in ids
end
test "lists only enabled templates" do
t1 = online_store_template(%{name: "Amazon", enabled: true})
t2 = online_store_template(%{name: "Bandcamp", enabled: false})
enabled = OnlineStoreTemplates.list_enabled_templates()
ids = Enum.map(enabled, & &1.id)
assert t1.id in ids
refute t2.id in ids
end
end
describe "create_template/1" do
test "creates with valid attrs" do
assert {:ok, template} =
OnlineStoreTemplates.create_template(%{
name: "Test Store",
url_template: "https://example.com/search?q={artist}"
})
assert template.name == "Test Store"
assert template.enabled == true
end
test "returns error with invalid attrs" do
assert {:error, changeset} = OnlineStoreTemplates.create_template(%{name: nil})
assert %{name: ["can't be blank"]} = errors_on(changeset)
end
test "returns error with invalid URL template" do
assert {:error, changeset} =
OnlineStoreTemplates.create_template(%{
name: "Bad",
url_template: "not-a-url"
})
assert %{url_template: ["must be a valid HTTP or HTTPS URL"]} = errors_on(changeset)
end
end
describe "update_template/2" do
test "updates with valid attrs" do
template = online_store_template()
assert {:ok, updated} =
OnlineStoreTemplates.update_template(template, %{name: "Updated Name"})
assert updated.name == "Updated Name"
end
end
describe "delete_template/1" do
test "deletes the template" do
template = online_store_template()
assert {:ok, _} = OnlineStoreTemplates.delete_template(template)
assert_raise Ecto.NoResultsError, fn -> OnlineStoreTemplates.get_template!(template.id) end
end
end
describe "generate_url/2" do
test "generates URL from template and record" do
template =
online_store_template(%{
url_template: "https://example.com/search?q={artist}+{title}+{format}"
})
rec = record_with_artist("Pink Floyd", %{title: "The Wall", format: :vinyl})
url = OnlineStoreTemplates.generate_url(template, rec)
assert url =~ "Pink+Floyd"
assert url =~ "The+Wall"
assert url =~ "vinyl"
assert String.starts_with?(url, "https://example.com/search?q=")
end
end
end
+210
View File
@@ -0,0 +1,210 @@
defmodule MusicLibrary.RecordSetsTest do
use MusicLibrary.DataCase
import MusicLibrary.Fixtures.Records
import MusicLibrary.Fixtures.RecordSets
alias MusicLibrary.RecordSets
describe "search_record_sets/2" do
test "returns sets matching by name" do
set = record_set(%{name: "Road Trip Mix"})
_other = record_set(%{name: "Unrelated"})
results = RecordSets.search_record_sets("Road Trip")
assert Enum.any?(results, &(&1.id == set.id))
refute Enum.any?(results, &(&1.name == "Unrelated"))
end
test "returns sets matching by description" do
set = record_set(%{name: "My Set", description: "Great progressive rock"})
_other = record_set(%{name: "Other", description: "Nothing here"})
results = RecordSets.search_record_sets("progressive")
assert Enum.any?(results, &(&1.id == set.id))
end
test "returns sets matching by record title" do
{set, [rec | _]} = record_set_with_records(1, %{name: "Favorites"})
results = RecordSets.search_record_sets(rec.title)
assert Enum.any?(results, &(&1.id == set.id))
end
test "returns sets matching by artist name" do
rec = record_with_artist("Marillion")
set = record_set(%{name: "Prog"})
{:ok, _} = RecordSets.add_record_to_set(set, rec.id)
results = RecordSets.search_record_sets("Marillion")
assert Enum.any?(results, &(&1.id == set.id))
end
test "returns all sets when query is empty" do
set1 = record_set(%{name: "First"})
set2 = record_set(%{name: "Second"})
results = RecordSets.search_record_sets("")
ids = Enum.map(results, & &1.id)
assert set1.id in ids
assert set2.id in ids
end
test "respects offset and limit" do
for i <- 1..5, do: record_set(%{name: "Set #{i}"})
results = RecordSets.search_record_sets("", limit: 2)
assert length(results) == 2
all = RecordSets.search_record_sets("")
offset_results = RecordSets.search_record_sets("", offset: 2, limit: 2)
refute Enum.at(all, 0).id == Enum.at(offset_results, 0).id
end
test "orders by updated_at desc by default" do
s1 = record_set(%{name: "Alpha"})
s2 = record_set(%{name: "Beta"})
# Manually set timestamps to ensure deterministic ordering
past = ~U[2024-01-01 00:00:00Z]
future = ~U[2025-01-01 00:00:00Z]
Repo.update_all(from(rs in "record_sets", where: rs.id == ^s1.id), set: [updated_at: past])
Repo.update_all(from(rs in "record_sets", where: rs.id == ^s2.id),
set: [updated_at: future]
)
results = RecordSets.search_record_sets("")
ids = Enum.map(results, & &1.id)
# s2 has newer updated_at so it should be first
assert Enum.find_index(ids, &(&1 == s2.id)) < Enum.find_index(ids, &(&1 == s1.id))
end
test "orders alphabetically when order: :alphabetical" do
record_set(%{name: "Zulu"})
record_set(%{name: "Alpha"})
results = RecordSets.search_record_sets("", order: :alphabetical)
names = Enum.map(results, & &1.name)
assert Enum.find_index(names, &(&1 == "Alpha")) < Enum.find_index(names, &(&1 == "Zulu"))
end
end
describe "count_record_sets/0 and count_record_sets/1" do
test "returns total count" do
assert RecordSets.count_record_sets() == 0
record_set(%{name: "One"})
record_set(%{name: "Two"})
assert RecordSets.count_record_sets() == 2
end
test "returns count matching query" do
record_set(%{name: "Road Trip"})
record_set(%{name: "Sunday Morning"})
assert RecordSets.count_record_sets("Road") == 1
assert RecordSets.count_record_sets("") == 2
end
end
describe "create_record_set/1" do
test "creates with valid attrs" do
assert {:ok, set} = RecordSets.create_record_set(%{name: "My Set"})
assert set.name == "My Set"
assert set.items == []
end
test "returns error changeset with invalid attrs" do
assert {:error, changeset} = RecordSets.create_record_set(%{name: nil})
assert %{name: ["can't be blank"]} = errors_on(changeset)
end
end
describe "update_record_set/2" do
test "updates with valid attrs" do
set = record_set(%{name: "Old Name"})
assert {:ok, updated} = RecordSets.update_record_set(set, %{name: "New Name"})
assert updated.name == "New Name"
end
end
describe "delete_record_set/1" do
test "deletes the record set" do
set = record_set()
assert {:ok, _} = RecordSets.delete_record_set(set)
assert_raise Ecto.NoResultsError, fn -> RecordSets.get_record_set!(set.id) end
end
end
describe "add_record_to_set/2" do
test "adds a record to the set" do
set = record_set()
rec = record()
assert {:ok, updated} = RecordSets.add_record_to_set(set, rec.id)
assert length(updated.items) == 1
assert hd(updated.items).record.id == rec.id
end
test "returns error on duplicate" do
set = record_set()
rec = record()
{:ok, _} = RecordSets.add_record_to_set(set, rec.id)
assert {:error, _changeset} = RecordSets.add_record_to_set(set, rec.id)
end
end
describe "remove_record_from_set/2" do
test "removes a record and recompacts positions" do
{set, [r1, r2, r3]} = record_set_with_records(3)
{:ok, updated} = RecordSets.remove_record_from_set(set, r2.id)
record_ids = Enum.map(updated.items, & &1.record.id)
assert r1.id in record_ids
assert r3.id in record_ids
refute r2.id in record_ids
positions = Enum.map(updated.items, & &1.position)
assert positions == [0, 1]
end
end
describe "move_record_in_set/3" do
test "moves a record up" do
{set, [r1, r2 | _]} = record_set_with_records(3)
{:ok, updated} = RecordSets.move_record_in_set(set, r2.id, :up)
ids_in_order = Enum.map(updated.items, & &1.record.id)
assert Enum.at(ids_in_order, 0) == r2.id
assert Enum.at(ids_in_order, 1) == r1.id
end
test "moves a record down" do
{set, [r1, r2 | _]} = record_set_with_records(3)
{:ok, updated} = RecordSets.move_record_in_set(set, r1.id, :down)
ids_in_order = Enum.map(updated.items, & &1.record.id)
assert Enum.at(ids_in_order, 0) == r2.id
assert Enum.at(ids_in_order, 1) == r1.id
end
test "no-ops at boundaries" do
{set, [r1, _, r3]} = record_set_with_records(3)
{:ok, moved_up} = RecordSets.move_record_in_set(set, r1.id, :up)
assert Enum.map(moved_up.items, & &1.record.id) == Enum.map(set.items, & &1.record.id)
{:ok, moved_down} = RecordSets.move_record_in_set(set, r3.id, :down)
assert Enum.map(moved_down.items, & &1.record.id) == Enum.map(set.items, & &1.record.id)
end
end
end
+77
View File
@@ -0,0 +1,77 @@
defmodule MusicLibrary.SearchTest do
use MusicLibrary.DataCase
import MusicLibrary.Fixtures.Records
alias MusicLibrary.Search
defp create_records(_) do
collected =
record_with_artist("Marillion", %{title: "Brave", purchased_at: DateTime.utc_now()})
wishlisted =
record_with_artist("Marillion", %{title: "Afraid of Sunlight", purchased_at: nil})
# Create artist info so search_artists can find artists
[artist] = collected.artists
artist_info(artist.musicbrainz_id)
%{collected: collected, wishlisted: wishlisted}
end
describe "universal_search/2" do
setup [:create_records]
test "returns collection, wishlist, and artist results", %{
collected: collected,
wishlisted: wishlisted
} do
results = Search.universal_search("Marillion")
collection_ids = Enum.map(results.collection, & &1.id)
assert collected.id in collection_ids
wishlist_ids = Enum.map(results.wishlist, & &1.id)
assert wishlisted.id in wishlist_ids
artist_names = Enum.map(results.artists, fn a -> a.artist.name end)
assert "Marillion" in artist_names
end
test "returns empty results for no matches" do
results = Search.universal_search("zzz_nonexistent_zzz")
assert results.collection == []
assert results.wishlist == []
assert results.artists == []
end
test "respects limit", %{collected: _collected} do
results = Search.universal_search("Marillion", limit: 1)
assert length(results.collection) <= 1
assert length(results.wishlist) <= 1
assert length(results.artists) <= 1
end
end
describe "search_counts/1" do
setup [:create_records]
test "returns counts per category" do
counts = Search.search_counts("Marillion")
assert counts.collection_count >= 1
assert counts.wishlist_count >= 1
assert counts.artists_count >= 1
end
test "returns zero counts for no matches" do
counts = Search.search_counts("zzz_nonexistent_zzz")
assert counts.collection_count == 0
assert counts.wishlist_count == 0
assert counts.artists_count == 0
end
end
end
@@ -0,0 +1,108 @@
defmodule MusicLibraryWeb.OnlineStoreTemplateLive.IndexTest do
use MusicLibraryWeb.ConnCase
import MusicLibrary.Fixtures.OnlineStoreTemplates
import Phoenix.LiveViewTest
alias MusicLibrary.OnlineStoreTemplates
describe "Index" do
test "lists all templates", %{conn: conn} do
online_store_template(%{name: "Amazon UK"})
online_store_template(%{name: "Bandcamp"})
conn
|> visit(~p"/online-store-templates")
|> assert_has("h1", text: "Online Store Templates")
|> assert_has("p", text: "Amazon UK")
|> assert_has("p", text: "Bandcamp")
end
end
describe "Create template" do
test "creates with valid data", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/online-store-templates/new")
view
|> form("#online_store_template-form",
online_store_template: %{
name: "New Store",
url_template: "https://store.example.com/search?q={artist}+{title}"
}
)
|> render_submit()
templates = OnlineStoreTemplates.list_templates()
assert Enum.any?(templates, &(&1.name == "New Store"))
end
test "shows validation errors", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/online-store-templates/new")
html =
view
|> form("#online_store_template-form",
online_store_template: %{name: "", url_template: ""}
)
|> render_change()
assert html =~ "can&#39;t be blank"
end
end
describe "Edit template" do
test "updates with valid data", %{conn: conn} do
template = online_store_template(%{name: "Old Name"})
{:ok, view, _html} = live(conn, ~p"/online-store-templates/#{template}/edit")
view
|> form("#online_store_template-form",
online_store_template: %{name: "Updated Name"}
)
|> render_submit()
updated = OnlineStoreTemplates.get_template!(template.id)
assert updated.name == "Updated Name"
end
end
describe "Delete template" do
test "deletes from listing", %{conn: conn} do
template = online_store_template(%{name: "To Delete"})
{:ok, view, _html} = live(conn, ~p"/online-store-templates")
view
|> element("button[phx-click='delete'][phx-value-id='#{template.id}']")
|> render_click()
refute has_element?(view, "p", "To Delete")
assert_raise Ecto.NoResultsError, fn -> OnlineStoreTemplates.get_template!(template.id) end
end
end
describe "Toggle enabled" do
test "toggles template enabled status", %{conn: conn} do
template = online_store_template(%{name: "Toggle Me", enabled: true})
{:ok, view, _html} = live(conn, ~p"/online-store-templates")
# Toggle to disabled
view
|> element("button[phx-click='toggle-enabled'][phx-value-id='#{template.id}']")
|> render_click()
updated = OnlineStoreTemplates.get_template!(template.id)
refute updated.enabled
# Toggle back to enabled
view
|> element("button[phx-click='toggle-enabled'][phx-value-id='#{template.id}']")
|> render_click()
updated = OnlineStoreTemplates.get_template!(template.id)
assert updated.enabled
end
end
end
@@ -0,0 +1,158 @@
defmodule MusicLibraryWeb.RecordSetLive.IndexTest do
use MusicLibraryWeb.ConnCase
import MusicLibrary.Fixtures.RecordSets
import Phoenix.LiveViewTest
alias MusicLibrary.RecordSets
describe "Index" do
test "lists record sets with names and record counts", %{conn: conn} do
{_set, _records} = record_set_with_records(2, %{name: "My Favorites"})
conn
|> visit(~p"/record-sets")
|> assert_has("h1", text: "Record Sets")
|> assert_has("h2", text: "My Favorites")
end
test "shows empty state when no sets exist", %{conn: conn} do
conn
|> visit(~p"/record-sets")
|> assert_has("p", text: "No record sets yet")
end
end
describe "Search" do
test "filters sets by query", %{conn: conn} do
record_set(%{name: "Road Trip"})
record_set(%{name: "Unrelated Set"})
conn
|> visit(~p"/record-sets?query=Road")
|> assert_has("h2", text: "Road Trip")
|> refute_has("h2", text: "Unrelated Set")
end
end
describe "Ordering" do
test "switches to alphabetical via URL param", %{conn: conn} do
record_set(%{name: "Zulu Set"})
record_set(%{name: "Alpha Set"})
{:ok, _view, html} = live(conn, ~p"/record-sets?order=alphabetical")
alpha_pos = :binary.match(html, "Alpha Set")
zulu_pos = :binary.match(html, "Zulu Set")
assert alpha_pos < zulu_pos
end
end
describe "Create set" do
test "creates a set with valid data", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/record-sets/new")
view
|> form("#record-set-form", record_set: %{name: "Brand New Set"})
|> render_submit()
assert [set] = RecordSets.search_record_sets("Brand New Set")
assert set.name == "Brand New Set"
end
test "shows validation errors with invalid data", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/record-sets/new")
html =
view
|> form("#record-set-form", record_set: %{name: ""})
|> render_change()
assert html =~ "can&#39;t be blank"
end
end
describe "Edit set" do
test "updates set with valid data", %{conn: conn} do
set = record_set(%{name: "Old Name"})
{:ok, view, _html} = live(conn, ~p"/record-sets/#{set}/edit")
view
|> form("#record-set-form", record_set: %{name: "Updated Name"})
|> render_submit()
updated = RecordSets.get_record_set!(set.id)
assert updated.name == "Updated Name"
end
end
describe "Delete set" do
test "deletes a set from the listing", %{conn: conn} do
set = record_set(%{name: "To Delete"})
{:ok, view, _html} = live(conn, ~p"/record-sets")
view
|> element("button[phx-click='delete_set'][phx-value-id='#{set.id}']")
|> render_click()
refute has_element?(view, "h2", "To Delete")
assert_raise Ecto.NoResultsError, fn -> RecordSets.get_record_set!(set.id) end
end
end
describe "Remove record from set" do
test "removes a record from the set", %{conn: conn} do
{set, [r1 | _]} = record_set_with_records(2)
{:ok, view, _html} = live(conn, ~p"/record-sets")
view
|> element(
"button[phx-click='remove_record'][phx-value-set-id='#{set.id}'][phx-value-record-id='#{r1.id}']"
)
|> render_click()
updated = RecordSets.get_record_set!(set.id)
refute Enum.any?(updated.items, &(&1.record.id == r1.id))
end
end
describe "Reorder records" do
test "moves a record left (up)", %{conn: conn} do
{set, [r1, r2 | _]} = record_set_with_records(3)
{:ok, view, _html} = live(conn, ~p"/record-sets")
view
|> element(
"button[phx-click='move_up'][phx-value-set-id='#{set.id}'][phx-value-record-id='#{r2.id}']"
)
|> render_click()
updated = RecordSets.get_record_set!(set.id)
ids_in_order = Enum.map(updated.items, & &1.record.id)
assert Enum.at(ids_in_order, 0) == r2.id
assert Enum.at(ids_in_order, 1) == r1.id
end
test "moves a record right (down)", %{conn: conn} do
{set, [r1, r2 | _]} = record_set_with_records(3)
{:ok, view, _html} = live(conn, ~p"/record-sets")
view
|> element(
"button[phx-click='move_down'][phx-value-set-id='#{set.id}'][phx-value-record-id='#{r1.id}']"
)
|> render_click()
updated = RecordSets.get_record_set!(set.id)
ids_in_order = Enum.map(updated.items, & &1.record.id)
assert Enum.at(ids_in_order, 0) == r2.id
assert Enum.at(ids_in_order, 1) == r1.id
end
end
end
@@ -0,0 +1,24 @@
defmodule MusicLibrary.Fixtures.OnlineStoreTemplates do
@moduledoc """
This module defines test helpers for creating
entities via the `MusicLibrary.OnlineStoreTemplates` context.
"""
alias MusicLibrary.OnlineStoreTemplates
def online_store_template(attrs \\ %{}) do
n = System.unique_integer([:positive])
{:ok, template} =
attrs
|> Enum.into(%{
name: "Store #{n}",
description: "A test store template",
url_template: "https://example.com/search?q={artist}+{title}+{format}",
enabled: true
})
|> OnlineStoreTemplates.create_template()
template
end
end
@@ -0,0 +1,35 @@
defmodule MusicLibrary.Fixtures.RecordSets do
@moduledoc """
This module defines test helpers for creating
entities via the `MusicLibrary.RecordSets` context.
"""
alias MusicLibrary.RecordSets
def record_set(attrs \\ %{}) do
{:ok, record_set} =
attrs
|> Enum.into(%{
name: "Set #{System.unique_integer([:positive])}",
description: "A test record set"
})
|> RecordSets.create_record_set()
record_set
end
def record_set_with_records(n, attrs \\ %{}) do
import MusicLibrary.Fixtures.Records, only: [record: 1]
set = record_set(attrs)
records =
Enum.map(1..n, fn _ ->
rec = record(%{})
{:ok, _} = RecordSets.add_record_to_set(set, rec.id)
rec
end)
{RecordSets.get_record_set!(set.id), records}
end
end