fix(errors): LIKE wildcard escaping, memory-efficient counts, context tests

- Escape LIKE wildcards (% and _) in search queries using fragment ESCAPE
  to prevent accidental pattern expansion and potential DoS vectors
- Use Repo.aggregate for occurrence_count instead of loading all
  occurrences into memory in get_error/1
- Use Repo.aggregate(:min, :inserted_at) for first_occurrence_at instead
  of fragile List.last/1 that depends on query ordering
- Add MusicLibrary.ErrorsTest with 15 tests covering list_errors/1
  (default, status/muted/search filters, wildcard escaping, pagination,
  empty results) and get_error/1 (with/without occurrences, not_found)
This commit is contained in:
Claudio Ortolina
2026-05-04 14:11:46 +01:00
parent 26a9469b3f
commit 93ac86f748
2 changed files with 194 additions and 5 deletions
+16 -5
View File
@@ -62,8 +62,13 @@ defmodule MusicLibrary.Errors do
)
|> Repo.all()
occurrence_count = length(occurrences)
first_occurrence_at = get_first_occurrence_at(occurrences)
occurrence_count =
from(o in Occurrence, where: o.error_id == ^id)
|> Repo.aggregate(:count, :id)
first_occurrence_at =
from(o in Occurrence, where: o.error_id == ^id)
|> Repo.aggregate(:min, :inserted_at)
result =
%{error | occurrences: occurrences}
@@ -93,9 +98,15 @@ defmodule MusicLibrary.Errors do
defp maybe_filter_search(query, ""), do: query
defp maybe_filter_search(query, search) do
where(query, [e], like(e.reason, ^"%#{search}%"))
escaped = escape_like_wildcards(search)
where(query, [e], fragment("? LIKE ? ESCAPE '\\'", e.reason, ^"%#{escaped}%"))
end
defp get_first_occurrence_at([]), do: nil
defp get_first_occurrence_at(occurrences), do: List.last(occurrences).inserted_at
@doc false
def escape_like_wildcards(search) when is_binary(search) do
search
|> String.replace("\\", "\\\\")
|> String.replace("%", "\\%")
|> String.replace("_", "\\_")
end
end