Generate import slugs from an alphabet the slug format allows

dde1795 · Gabriel Morell · 2026-09-16 03:59

10 files +48 -7
Message
{commit_body(@commit)}

Files changed

modified lib/diogramos/diagrams/canvases.ex
+5 −2
@@ -215,8 +215,11 @@ defmodule Diogramos.Diagrams.Canvases do
215 215
216 216 base = if base == "", do: "canvas", else: base
217 217
218 − suffix =
219 − :crypto.strong_rand_bytes(3) |> Base.url_encode64(padding: false) |> String.downcase()
218 + # base32, not url_encode64: the URL-safe alphabet includes "_", which
219 + # `Canvas.create_changeset/2` rejects, so roughly one import in
220 + # sixteen failed on a slug it had generated itself. base32's
221 + # alphabet lowercases into exactly the characters the slug allows.
222 + suffix = :crypto.strong_rand_bytes(3) |> Base.encode32(case: :lower, padding: false)
220 223
221 224 base <> "-" <> suffix
222 225 end
modified test/diogramos/diagrams_test.exs
+35 −0
@@ -276,4 +276,39 @@ defmodule Diogramos.DiagramsTest do
276 276 assert Diagrams.authorize(b, :admin, f) == {:error, :forbidden}
277 277 end
278 278 end
279 +
280 + describe "generated slugs" do
281 + test "always satisfy the format the changeset enforces" do
282 + scope = Diogramos.AccountsFixtures.user_scope_fixture()
283 +
284 + payload = %{
285 + "format" => "diogramos.canvas.v1",
286 + "title" => "Round-trip",
287 + "theme" => "afterdark",
288 + "document" => %{"elements" => %{}, "order" => [], "connectors" => %{}}
289 + }
290 +
291 + # The suffix is random, and the previous alphabet produced an
292 + # invalid slug about one time in sixteen -- often enough to fail a
293 + # build, rarely enough to look flaky rather than broken.
294 + for _ <- 1..200 do
295 + assert {:ok, canvas} = Diagrams.import_canvas(scope, payload)
296 + assert canvas.slug =~ ~r/^[a-z0-9][a-z0-9-]{0,63}$/
297 + end
298 + end
299 +
300 + test "a title with nothing usable in it still yields a valid slug" do
301 + scope = Diogramos.AccountsFixtures.user_scope_fixture()
302 +
303 + payload = %{
304 + "format" => "diogramos.canvas.v1",
305 + "title" => "!!! ???",
306 + "theme" => "afterdark",
307 + "document" => %{"elements" => %{}, "order" => [], "connectors" => %{}}
308 + }
309 +
310 + assert {:ok, canvas} = Diagrams.import_canvas(scope, payload)
311 + assert canvas.slug =~ ~r/^[a-z0-9][a-z0-9-]{0,63}$/
312 + end
313 + end
279 314 end
modified test/diogramos_web/channels/canvas_channel_test.exs
+8 −5
@@ -17,7 +17,10 @@ defmodule DiogramosWeb.CanvasChannelTest do
17 17
18 18 defp connect_as(user) do
19 19 token = Accounts.create_api_token(user)
20 − {:ok, socket} = Phoenix.ChannelTest.connect(UserSocket, %{"token" => token}, %{})
20 +
21 + {:ok, socket} =
22 + Phoenix.ChannelTest.connect(UserSocket, %{"token" => token}, connect_info: %{})
23 +
21 24 socket
22 25 end
23 26
@@ -33,14 +36,14 @@ defmodule DiogramosWeb.CanvasChannelTest do
33 36 describe "socket connect" do
34 37 test "accepts a valid api token", %{user: user} do
35 38 token = Accounts.create_api_token(user)
36 − assert {:ok, socket} = connect(UserSocket, %{"token" => token}, %{})
39 + assert {:ok, socket} = connect(UserSocket, %{"token" => token}, connect_info: %{})
37 40 assert socket.assigns.user_id == user.id
38 41 assert UserSocket.id(socket) == "user_socket:#{user.id}"
39 42 end
40 43
41 44 test "rejects a bogus or missing token" do
42 − assert :error = connect(UserSocket, %{"token" => "nope"}, %{})
43 − assert :error = connect(UserSocket, %{}, %{})
45 + assert :error = connect(UserSocket, %{"token" => "nope"}, connect_info: %{})
46 + assert :error = connect(UserSocket, %{}, connect_info: %{})
44 47 end
45 48
46 49 test "rejects a revoked token", %{user: user} do
@@ -48,7 +51,7 @@ defmodule DiogramosWeb.CanvasChannelTest do
48 51 {_user, token_id} = Accounts.fetch_user_by_api_token(token)
49 52 :ok = Accounts.delete_api_token(user, token_id)
50 53
51 − assert :error = connect(UserSocket, %{"token" => token}, %{})
54 + assert :error = connect(UserSocket, %{"token" => token}, connect_info: %{})
52 55 end
53 56 end
54 57

Parents: e380962