fix: D1-2 correct post translation unique_constraint name so duplicate-language violations return a changeset error
This commit is contained in:
@@ -114,7 +114,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
|
|||||||
| ID | Claim | Spec | Path |
|
| ID | Claim | Spec | Path |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| D1-1 | ~~UniqueMediaTranslation invariant~~ | media.allium:108 | **Resolved:** test added (re-upsert updates not duplicates; direct duplicate insert rejected). Test exposed a real bug — `Media.Translation` declared the migration's index name but ecto_sqlite3 derives the violated-constraint name from columns, so violations crashed instead of returning a changeset error; fixed `unique_constraint` name to `:media_translations_translation_for_language_index` |
|
| D1-1 | ~~UniqueMediaTranslation invariant~~ | media.allium:108 | **Resolved:** test added (re-upsert updates not duplicates; direct duplicate insert rejected). Test exposed a real bug — `Media.Translation` declared the migration's index name but ecto_sqlite3 derives the violated-constraint name from columns, so violations crashed instead of returning a changeset error; fixed `unique_constraint` name to `:media_translations_translation_for_language_index` |
|
||||||
| D1-2 | UniqueTranslationPerLanguage invariant | translation.allium:94 | Write test: create duplicate post translation, expect rejection |
|
| D1-2 | ~~UniqueTranslationPerLanguage invariant~~ | translation.allium:94 | **Resolved:** test added (re-upsert updates not duplicates; direct duplicate insert rejected). Same bug as D1-1 — `Posts.Translation` declared the migration's index name but ecto_sqlite3 derives the violated-constraint name from columns, so duplicates crashed instead of returning a changeset error; fixed `unique_constraint` name to `:post_translations_translation_for_language_index` |
|
||||||
| D1-3 | BundledDefaultTemplatesExistOutsideProjectData | template.allium:65 | Write test: render with no Template rows, bundled template found |
|
| D1-3 | BundledDefaultTemplatesExistOutsideProjectData | template.allium:65 | Write test: render with no Template rows, bundled template found |
|
||||||
| D1-4 | UserTemplateDirectoryOverridesBundledDefaults | template.allium:75 | Write test: project template overrides bundled same-slug |
|
| D1-4 | UserTemplateDirectoryOverridesBundledDefaults | template.allium:75 | Write test: project template overrides bundled same-slug |
|
||||||
| D1-5 | LiquidTagSubset (5 tags only) | template.allium:179 | Write test: unsupported tag raises error |
|
| D1-5 | LiquidTagSubset (5 tags only) | template.allium:179 | Write test: unsupported tag raises error |
|
||||||
|
|||||||
@@ -79,6 +79,6 @@ defmodule BDS.Posts.Translation do
|
|||||||
:updated_at
|
:updated_at
|
||||||
])
|
])
|
||||||
|> foreign_key_constraint(:translation_for)
|
|> foreign_key_constraint(:translation_for)
|
||||||
|> unique_constraint(:language, name: :post_translations_translation_language_idx)
|
|> unique_constraint(:language, name: :post_translations_translation_for_language_index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -310,6 +310,61 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
assert report.do_not_translate_posts == [ignored_post.id]
|
assert report.do_not_translate_posts == [ignored_post.id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "UniqueTranslationPerLanguage: a post has at most one translation per language", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
assert {:ok, post} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Canonical Post",
|
||||||
|
content: "Hello world",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, first} =
|
||||||
|
Posts.upsert_post_translation(post.id, "de", %{title: "Titel"})
|
||||||
|
|
||||||
|
# Re-upserting the same language updates the existing row instead of adding a second.
|
||||||
|
assert {:ok, second} =
|
||||||
|
Posts.upsert_post_translation(post.id, "de", %{title: "Geändert"})
|
||||||
|
|
||||||
|
assert second.id == first.id
|
||||||
|
|
||||||
|
assert [persisted] =
|
||||||
|
BDS.Posts.Translation
|
||||||
|
|> where([t], t.translation_for == ^post.id and t.language == "de")
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
assert persisted.title == "Geändert"
|
||||||
|
|
||||||
|
# A direct insert of a distinct row with the same (post, language) is rejected by the
|
||||||
|
# unique constraint backing the invariant, returning a changeset error rather than crashing.
|
||||||
|
now = BDS.Persistence.now_ms()
|
||||||
|
|
||||||
|
duplicate =
|
||||||
|
BDS.Posts.Translation.changeset(%BDS.Posts.Translation{}, %{
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
project_id: post.project_id,
|
||||||
|
translation_for: post.id,
|
||||||
|
language: "de",
|
||||||
|
title: "Duplicate",
|
||||||
|
status: :draft,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:error, changeset} = Repo.insert(duplicate)
|
||||||
|
assert %{language: ["has already been taken"]} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp errors_on(changeset) do
|
||||||
|
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||||
|
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
||||||
|
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp configure_auto_translation_test_runtime do
|
defp configure_auto_translation_test_runtime do
|
||||||
assert {:ok, _endpoint} =
|
assert {:ok, _endpoint} =
|
||||||
AI.put_endpoint(
|
AI.put_endpoint(
|
||||||
|
|||||||
Reference in New Issue
Block a user