···8585 assert result =~ "- no route from [@dave.test](https://bsky.app/profile/did:plc:dave)"
8686 end
87878888- test "falls back to DID when handle is nil" do
8989- routes = [{@alice_did, nil, {:direct, @author_did}}]
9090- result = CommentBuilder.build_comment(@author_did, nil, routes)
9191-9292- refute result =~ "##"
9393- assert result =~ "- [#{@alice_did}](https://bsky.app/profile/#{@alice_did}) -> [#{@author_did}](https://bsky.app/profile/#{@author_did})"
9494- end
9595-9696- test "falls back to DID when handle is empty string" do
9797- routes = [{@alice_did, "", {:direct, @author_did}}]
9898- result = CommentBuilder.build_comment(@author_did, "", routes)
8888+ test "falls back to DID when handle is nil or empty string" do
8989+ for maintainer_handle <- [nil, ""] do
9090+ routes = [{@alice_did, maintainer_handle, {:direct, @author_did}}]
9191+ result = CommentBuilder.build_comment(@author_did, maintainer_handle, routes)
9992100100- refute result =~ "##"
101101- assert result =~ "- [#{@alice_did}](https://bsky.app/profile/#{@alice_did}) -> [#{@author_did}](https://bsky.app/profile/#{@author_did})"
9393+ refute result =~ "##"
9494+ assert result =~ "- [#{@alice_did}](https://bsky.app/profile/#{@alice_did}) -> [#{@author_did}](https://bsky.app/profile/#{@author_did})"
9595+ end
10296 end
1039710498 test "formats three-hop path" do
-24
appview/test/atvouch/tangled/scraper_test.exs
···4848 """
4949 end
50505151- test "parses pulls page HTML to extract PR list with timestamps" do
5252- html =
5353- wrap_page(
5454- pr_entry("alice.test", "3abc123", 3, "Fix broken tests", "2026-03-19T14:37:19+00:00") <>
5555- pr_entry("alice.test", "3abc123", 2, "Add new feature", "2026-03-10T16:25:26+00:00") <>
5656- pr_entry("alice.test", "3abc123", 1, "Initial setup", "2026-02-15T10:00:00+00:00")
5757- )
5858-5959- pulls = Scraper.parse_pulls_page(html)
6060-6161- assert length(pulls) == 3
6262- {3, title3, ts3} = Enum.at(pulls, 0)
6363- assert title3 =~ "Fix broken tests"
6464- assert ts3 == "2026-03-19T14:37:19+00:00"
6565-6666- {2, title2, ts2} = Enum.at(pulls, 1)
6767- assert title2 =~ "Add new feature"
6868- assert ts2 == "2026-03-10T16:25:26+00:00"
6969-7070- {1, title1, ts1} = Enum.at(pulls, 2)
7171- assert title1 =~ "Initial setup"
7272- assert ts1 == "2026-02-15T10:00:00+00:00"
7373- end
7474-7551 test "handles empty pulls page" do
7652 html = wrap_page("<p>No pull requests found.</p>")
7753 pulls = Scraper.parse_pulls_page(html)
+2-16
appview/test/atvouch/tangled/session_test.exs
···1515 {:ok, tangled_port: servers.tangled_port}
1616 end
17171818- test "successful login returns cookies", %{tangled_port: tangled_port} do
1919- {:ok, pid} =
2020- Session.start_link(
2121- handle: "bot.test",
2222- password: "test-password",
2323- tangled_url: "http://127.0.0.1:#{tangled_port}",
2424- name: :"session_test_#{tangled_port}"
2525- )
2626-2727- result = Session.get_cookies(pid)
2828- assert {:ok, cookies} = result
2929- assert String.contains?(cookies, "appview-session-v2=")
3030- end
3131-3232- test "cookies are cached on second call", %{tangled_port: tangled_port} do
1818+ test "successful login returns cookies and caches them", %{tangled_port: tangled_port} do
3319 {:ok, pid} =
3420 Session.start_link(
3521 handle: "bot.test",
···3925 )
40264127 {:ok, cookies1} = Session.get_cookies(pid)
2828+ assert String.contains?(cookies1, "appview-session-v2=")
42294330 # Drain messages from first login
4431 drain_messages()
45324633 {:ok, cookies2} = Session.get_cookies(pid)
4747-4834 assert cookies1 == cookies2
49355036 # Should NOT have received another login attempt
+2-15
appview/test/atvouch/xrpc_get_routes_test.exs
···4040 end
41414242 describe "GET /xrpc/dev.atvouch.graph.getRoutes" do
4343- test "returns 401 without authentication" do
4444- conn =
4545- conn(:get, "/xrpc/dev.atvouch.graph.getRoutes?target=did:plc:bob")
4646- |> Atvouch.Router.call(@opts)
4747-4848- assert conn.status == 401
4949- end
5050-5151- test "returns 401 with invalid token" do
5252- conn =
5353- conn(:get, "/xrpc/dev.atvouch.graph.getRoutes?target=did:plc:bob")
5454- |> put_req_header("authorization", "Bearer bad-token")
5555- |> Atvouch.Router.call(@opts)
5656-5757- assert conn.status == 401
4343+ test "requires authentication" do
4444+ assert_requires_auth("/xrpc/dev.atvouch.graph.getRoutes?target=did:plc:bob", @opts)
5845 end
59466047 test "returns 400 when target parameter is missing" do
+16-112
appview/test/atvouch/xrpc_vouches_test.exs
···7878 assert body["vouches"] == []
7979 end
80808181- test "returns 401 without authentication" do
8282- conn =
8383- conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches")
8484- |> Atvouch.Router.call(@opts)
8585-8686- assert conn.status == 401
8787- end
8888-8989- test "returns 401 with invalid token" do
9090- conn =
9191- conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches")
9292- |> put_req_header("authorization", "Bearer bad-token")
9393- |> Atvouch.Router.call(@opts)
9494-9595- assert conn.status == 401
8181+ test "requires authentication" do
8282+ assert_requires_auth("/xrpc/dev.atvouch.graph.getCurrentUserVouches", @opts)
9683 end
97849885 test "returns pagination data and behaves with limit" do
···152139 refute body["cursor"]
153140 end
154141155155- test "returns 400 with invalid cursor" do
156156- conn =
157157- conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches?cursor=notanumber")
158158- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
159159- |> Atvouch.Router.call(@opts)
160160-161161- assert conn.status == 400
162162- end
163163-164164- test "returns 400 with limit over 1000" do
165165- conn =
166166- conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches?limit=1001")
167167- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
168168- |> Atvouch.Router.call(@opts)
169169-170170- assert conn.status == 400
171171- end
172172-173173- test "returns 400 with limit under 1" do
174174- conn =
175175- conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches?limit=0")
176176- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
177177- |> Atvouch.Router.call(@opts)
178178-179179- assert conn.status == 400
142142+ test "validates pagination parameters" do
143143+ assert_validates_pagination_params(
144144+ "/xrpc/dev.atvouch.graph.getCurrentUserVouches", @opts,
145145+ auth_did: "did:plc:alice"
146146+ )
180147 end
181148 end
182149···231198 assert vouch["targetDid"] == "did:plc:carol"
232199 end
233200234234- test "returns 401 without authentication" do
235235- conn =
236236- conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches")
237237- |> Atvouch.Router.call(@opts)
238238-239239- assert conn.status == 401
240240- end
241241-242242- test "returns 401 with invalid token" do
243243- conn =
244244- conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches")
245245- |> put_req_header("authorization", "Bearer bad-token")
246246- |> Atvouch.Router.call(@opts)
247247-248248- assert conn.status == 401
201201+ test "requires authentication" do
202202+ assert_requires_auth("/xrpc/dev.atvouch.graph.getRemoteVouches", @opts)
249203 end
250204251205 test "returns total count" do
···335289 refute body["cursor"]
336290 end
337291338338- test "returns 400 with invalid cursor" do
339339- conn =
340340- conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches?cursor=notanumber")
341341- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
342342- |> Atvouch.Router.call(@opts)
343343-344344- assert conn.status == 400
345345- end
346346-347347- test "returns 400 with limit over 1000" do
348348- conn =
349349- conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches?limit=1001")
350350- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
351351- |> Atvouch.Router.call(@opts)
352352-353353- assert conn.status == 400
354354- end
355355-356356- test "returns 400 with limit under 1" do
357357- conn =
358358- conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches?limit=0")
359359- |> put_req_header("authorization", "Bearer valid-token:did:plc:alice")
360360- |> Atvouch.Router.call(@opts)
361361-362362- assert conn.status == 400
292292+ test "validates pagination parameters" do
293293+ assert_validates_pagination_params(
294294+ "/xrpc/dev.atvouch.graph.getRemoteVouches", @opts,
295295+ auth_did: "did:plc:alice"
296296+ )
363297 end
364298 end
365299···382316 "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:carol",
383317 "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:bob"
384318 ]
385385- end
386386-387387- test "returns total count of vouches" do
388388- conn =
389389- conn(:get, "/xrpc/dev.atvouch.graph.getEntireGraph")
390390- |> Atvouch.Router.call(@opts)
391391-392392- assert conn.status == 200
393393- body = Jason.decode!(conn.resp_body)
394394- assert body["total"] == 3
395319 end
396320397321 test "total count is consistent across paginated requests" do
···478402 assert length(body["vouches"]) == 3
479403 end
480404481481- test "returns 400 with invalid cursor" do
482482- conn =
483483- conn(:get, "/xrpc/dev.atvouch.graph.getEntireGraph?cursor=notanumber")
484484- |> Atvouch.Router.call(@opts)
485485-486486- assert conn.status == 400
487487- end
488488-489489- test "returns 400 with limit over 1000" do
490490- conn =
491491- conn(:get, "/xrpc/dev.atvouch.graph.getEntireGraph?limit=1001")
492492- |> Atvouch.Router.call(@opts)
493493-494494- assert conn.status == 400
495495- end
496496-497497- test "returns 400 with limit under 1" do
498498- conn =
499499- conn(:get, "/xrpc/dev.atvouch.graph.getEntireGraph?limit=0")
500500- |> Atvouch.Router.call(@opts)
501501-502502- assert conn.status == 400
405405+ test "validates pagination parameters" do
406406+ assert_validates_pagination_params("/xrpc/dev.atvouch.graph.getEntireGraph", @opts)
503407 end
504408 end
505409end
+41
appview/test/support/test_helpers.ex
···219219 }
220220 end
221221222222+ # --- Auth & Pagination Assertions ---
223223+224224+ def assert_requires_auth(path, router_opts) do
225225+ import ExUnit.Assertions
226226+ import Plug.Test
227227+ import Plug.Conn
228228+229229+ conn_no_auth = conn(:get, path) |> Atvouch.Router.call(router_opts)
230230+ assert conn_no_auth.status == 401, "expected 401 without auth for #{path}"
231231+232232+ conn_bad_token =
233233+ conn(:get, path)
234234+ |> put_req_header("authorization", "Bearer bad-token")
235235+ |> Atvouch.Router.call(router_opts)
236236+237237+ assert conn_bad_token.status == 401, "expected 401 with invalid token for #{path}"
238238+ end
239239+240240+ def assert_validates_pagination_params(path, router_opts, opts \\ []) do
241241+ import ExUnit.Assertions
242242+ import Plug.Test
243243+ import Plug.Conn
244244+245245+ auth_did = Keyword.get(opts, :auth_did)
246246+ sep = if String.contains?(path, "?"), do: "&", else: "?"
247247+248248+ make_conn = fn url ->
249249+ c = conn(:get, url)
250250+ if auth_did, do: put_req_header(c, "authorization", "Bearer valid-token:#{auth_did}"), else: c
251251+ end
252252+253253+ conn1 = make_conn.("#{path}#{sep}cursor=notanumber") |> Atvouch.Router.call(router_opts)
254254+ assert conn1.status == 400, "expected 400 for invalid cursor on #{path}"
255255+256256+ conn2 = make_conn.("#{path}#{sep}limit=1001") |> Atvouch.Router.call(router_opts)
257257+ assert conn2.status == 400, "expected 400 for limit over 1000 on #{path}"
258258+259259+ conn3 = make_conn.("#{path}#{sep}limit=0") |> Atvouch.Router.call(router_opts)
260260+ assert conn3.status == 400, "expected 400 for limit under 1 on #{path}"
261261+ end
262262+222263 # --- Drain Messages ---
223264224265 def drain_messages(timeout \\ 100) do