dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

simplify tests

authored by

Luna and committed by tangled.org 1ab8eae9 bf4d405d

+574 -1036
+90 -357
appview/test/atvouch/pull_handler_test.exs
··· 1 1 defmodule Atvouch.PullHandlerTest do 2 2 use ExUnit.Case 3 + import Atvouch.Test.Helpers 3 4 4 5 setup do 5 - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 6 - Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 7 - 8 - # Start fake Tangled server 9 - {tangled_pid, tangled_port, state_agent} = 10 - Atvouch.Test.FakeTangledServer.start(self()) 11 - 12 - # Start fake PDS server 13 - {pds_pid, pds_port} = 14 - Atvouch.Test.FakePdsServer.start(self(), 15 - expected_username: "bot.test", 16 - expected_password: "test-password", 17 - callback_url: "http://127.0.0.1:#{tangled_port}/oauth/callback?code=test&state=test" 18 - ) 19 - 20 - # Wire them together 21 - Atvouch.Test.FakeTangledServer.set_pds_url(state_agent, "http://127.0.0.1:#{pds_port}") 22 - 23 - tangled_url = "http://127.0.0.1:#{tangled_port}" 6 + checkout_sandbox() 24 7 25 - # Ensure no leftover session from previous test 26 - case GenServer.whereis(Atvouch.Tangled.Session) do 27 - nil -> :ok 28 - old_pid -> 29 - GenServer.stop(old_pid, :normal, 1_000) 30 - Process.sleep(10) 31 - end 8 + servers = start_tangled_pds_session(self()) 32 9 33 - # Start Session GenServer with the global name so TapHandler can find it 34 - {:ok, session_pid} = 35 - Atvouch.Tangled.Session.start_link( 36 - handle: "bot.test", 37 - password: "test-password", 38 - tangled_url: tangled_url, 39 - name: Atvouch.Tangled.Session 10 + prev_tangled = 11 + set_env(:tangled, 12 + url: servers.tangled_url, 13 + bot_handle: "bot.test", 14 + bot_password: "test-password" 40 15 ) 41 16 42 - # Configure tangled settings in app env 43 - prev_tangled = Application.get_env(:atvouch, :tangled) 44 - 45 - Application.put_env(:atvouch, :tangled, 46 - url: tangled_url, 47 - bot_handle: "bot.test", 48 - bot_password: "test-password" 49 - ) 50 - 51 - # Start fake slingshot server with test identities 52 17 {slingshot_pid, slingshot_port, _slingshot_agent} = 53 18 Atvouch.Test.FakeSlingshotServer.start(%{ 54 19 "did:plc:maintainer1" => "maintainer1.test", ··· 59 24 "did:plc:repoowner2" => "repoowner2.test" 60 25 }) 61 26 62 - prev_slingshot_url = Application.get_env(:atvouch, :slingshot_url) 63 - Application.put_env(:atvouch, :slingshot_url, "http://127.0.0.1:#{slingshot_port}") 27 + prev_slingshot = set_env(:slingshot_url, "http://127.0.0.1:#{slingshot_port}") 64 28 65 - # Start fake TAP server 66 29 {tap_pid, tap_port} = Atvouch.Test.FakeTapServer.start(self()) 67 30 68 31 on_exit(fn -> 69 - Application.put_env(:atvouch, :tangled, prev_tangled) 70 - 71 - if prev_slingshot_url do 72 - Application.put_env(:atvouch, :slingshot_url, prev_slingshot_url) 73 - else 74 - Application.delete_env(:atvouch, :slingshot_url) 75 - end 32 + restore_env(:tangled, prev_tangled) 33 + restore_env(:slingshot_url, prev_slingshot) 76 34 77 - for pid <- [pds_pid, tangled_pid, tap_pid, slingshot_pid] do 78 - try do 79 - Supervisor.stop(pid, :normal, 1_000) 80 - catch 81 - :exit, _ -> :ok 82 - end 83 - end 35 + for pid <- [servers.pds_pid, servers.tangled_pid, tap_pid, slingshot_pid], 36 + do: safe_stop_supervisor(pid) 84 37 85 - try do 86 - GenServer.stop(session_pid, :normal, 1_000) 87 - catch 88 - :exit, _ -> :ok 89 - end 38 + safe_stop_genserver(servers.session_pid) 90 39 end) 91 40 92 41 {:ok, 93 42 tap_port: tap_port, 94 - tangled_port: tangled_port, 95 - state_agent: state_agent} 43 + tangled_port: servers.tangled_port, 44 + state_agent: servers.state_agent} 96 45 end 97 46 98 47 defp setup_vouch_graph do 99 48 # Create identities 100 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:maintainer1", handle: "maintainer1.test"}) 101 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:maintainer2", handle: "maintainer2.test"}) 102 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:author", handle: "author.test"}) 103 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:middle", handle: "middle.test"}) 104 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:repoowner", handle: "repoowner.test"}) 49 + for {did, handle} <- [ 50 + {"did:plc:maintainer1", "maintainer1.test"}, 51 + {"did:plc:maintainer2", "maintainer2.test"}, 52 + {"did:plc:author", "author.test"}, 53 + {"did:plc:middle", "middle.test"}, 54 + {"did:plc:repoowner", "repoowner.test"} 55 + ] do 56 + create_identity(did, handle) 57 + end 105 58 106 59 # Create vouches: maintainer1 -> author (direct), maintainer2 -> middle -> author (2-hop) 107 - {:ok, _} = 108 - Atvouch.Vouch.create(%{ 109 - at_uri: "at://did:plc:maintainer1/dev.atvouch.graph.vouch/did:plc:author", 110 - creator_did: "did:plc:maintainer1", 111 - target_did: "did:plc:author", 112 - original_created_at: "2026-03-01T00:00:00Z", 113 - remote_created_at: "2026-03-01T00:00:00Z", 114 - at_cid: "bafytest1", 115 - live: true 116 - }) 117 - 118 - {:ok, _} = 119 - Atvouch.Vouch.create(%{ 120 - at_uri: "at://did:plc:maintainer2/dev.atvouch.graph.vouch/did:plc:middle", 121 - creator_did: "did:plc:maintainer2", 122 - target_did: "did:plc:middle", 123 - original_created_at: "2026-03-01T00:00:00Z", 124 - remote_created_at: "2026-03-01T00:00:00Z", 125 - at_cid: "bafytest2", 126 - live: true 127 - }) 128 - 129 - {:ok, _} = 130 - Atvouch.Vouch.create(%{ 131 - at_uri: "at://did:plc:middle/dev.atvouch.graph.vouch/did:plc:author", 132 - creator_did: "did:plc:middle", 133 - target_did: "did:plc:author", 134 - original_created_at: "2026-03-01T00:00:00Z", 135 - remote_created_at: "2026-03-01T00:00:00Z", 136 - at_cid: "bafytest3", 137 - live: true 138 - }) 60 + create_vouch("did:plc:maintainer1", "did:plc:author", cid: "bafytest1") 61 + create_vouch("did:plc:maintainer2", "did:plc:middle", cid: "bafytest2") 62 + create_vouch("did:plc:middle", "did:plc:author", cid: "bafytest3") 139 63 140 64 # Create membership for the repo 141 65 repo_at_uri = "at://did:plc:repoowner/sh.tangled.repo/3abc123" ··· 185 109 } do 186 110 repo_at_uri = setup_vouch_graph() 187 111 188 - # Set up pulls page to show PR #1 189 112 Atvouch.Test.FakeTangledServer.set_pulls_html( 190 113 state_agent, 191 114 "repoowner.test", ··· 193 116 pulls_html("repoowner.test", "testrepo") 194 117 ) 195 118 196 - # Start TAP socket 197 - {:ok, _pid} = 198 - Atvouch.Tap.Socket.start_link( 199 - uri: "ws://localhost:#{tap_port}/channel", 200 - handler: Atvouch.TapHandler, 201 - password: "123", 202 - name: :"pull_handler_test_#{tap_port}" 203 - ) 204 - 205 - assert_receive {:ws_connected, ws_pid}, 5_000 119 + ws_pid = start_tap_socket(tap_port) 206 120 207 - # Send a pull create event 208 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 209 - "id" => 700, 210 - "type" => "record", 211 - "record" => %{ 212 - "action" => "create", 213 - "did" => "did:plc:author", 214 - "rev" => "3mgmqjki6sz2n", 215 - "collection" => "sh.tangled.repo.pull", 216 - "rkey" => "3pull123", 217 - "record" => %{ 218 - "$type" => "sh.tangled.repo.pull", 219 - "title" => "Fix tests", 220 - "createdAt" => "2026-03-19T10:00:00Z", 221 - "target" => %{ 222 - "repo" => repo_at_uri, 223 - "branch" => "main" 224 - } 225 - }, 226 - "cid" => "bafypull1", 227 - "live" => true 228 - } 229 - }) 121 + Atvouch.Test.FakeTapServer.send_event( 122 + ws_pid, 123 + pull_record_event(700, "did:plc:author", repo_at_uri, 124 + rkey: "3pull123", 125 + title: "Fix tests" 126 + ) 127 + ) 230 128 231 - # Wait for ack 232 129 assert_receive {:ws_message, %{"type" => "ack", "id" => 700}}, 10_000 233 130 234 131 # Verify comment was posted to Tangled ··· 246 143 end 247 144 248 145 test "skips pull for repo without membership", %{tap_port: tap_port} do 249 - {:ok, _pid} = 250 - Atvouch.Tap.Socket.start_link( 251 - uri: "ws://localhost:#{tap_port}/channel", 252 - handler: Atvouch.TapHandler, 253 - password: "123", 254 - name: :"pull_no_membership_test_#{tap_port}" 255 - ) 256 - 257 - assert_receive {:ws_connected, ws_pid}, 5_000 146 + ws_pid = start_tap_socket(tap_port) 258 147 259 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 260 - "id" => 701, 261 - "type" => "record", 262 - "record" => %{ 263 - "action" => "create", 264 - "did" => "did:plc:someone", 265 - "rev" => "3mgmqjki6sz2n", 266 - "collection" => "sh.tangled.repo.pull", 267 - "rkey" => "3pull456", 268 - "record" => %{ 269 - "$type" => "sh.tangled.repo.pull", 270 - "title" => "Some PR", 271 - "createdAt" => "2026-03-19T10:00:00Z", 272 - "target" => %{ 273 - "repo" => "at://did:plc:nobody/sh.tangled.repo/3xyz999", 274 - "branch" => "main" 275 - } 276 - }, 277 - "cid" => "bafypull2", 278 - "live" => true 279 - } 280 - }) 148 + Atvouch.Test.FakeTapServer.send_event( 149 + ws_pid, 150 + pull_record_event(701, "did:plc:someone", "at://did:plc:nobody/sh.tangled.repo/3xyz999", 151 + rkey: "3pull456", 152 + title: "Some PR" 153 + ) 154 + ) 281 155 282 156 assert_receive {:ws_message, %{"type" => "ack", "id" => 701}}, 5_000 283 157 ··· 299 173 pulls_html("repoowner.test", "testrepo", datetime: "2026-02-15T10:00:00+00:00") 300 174 ) 301 175 302 - {:ok, _pid} = 303 - Atvouch.Tap.Socket.start_link( 304 - uri: "ws://localhost:#{tap_port}/channel", 305 - handler: Atvouch.TapHandler, 306 - password: "123", 307 - name: :"pull_old_pr_test_#{tap_port}" 176 + ws_pid = start_tap_socket(tap_port) 177 + 178 + Atvouch.Test.FakeTapServer.send_event( 179 + ws_pid, 180 + pull_record_event(710, "did:plc:author", repo_at_uri, 181 + rkey: "3pullold", 182 + title: "Old PR before membership" 308 183 ) 309 - 310 - assert_receive {:ws_connected, ws_pid}, 5_000 311 - 312 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 313 - "id" => 710, 314 - "type" => "record", 315 - "record" => %{ 316 - "action" => "create", 317 - "did" => "did:plc:author", 318 - "rev" => "3mgmqjki6sz2n", 319 - "collection" => "sh.tangled.repo.pull", 320 - "rkey" => "3pullold", 321 - "record" => %{ 322 - "$type" => "sh.tangled.repo.pull", 323 - "title" => "Old PR before membership", 324 - "createdAt" => "2026-03-19T10:00:00Z", 325 - "target" => %{ 326 - "repo" => repo_at_uri, 327 - "branch" => "main" 328 - } 329 - }, 330 - "cid" => "bafypullold", 331 - "live" => true 332 - } 333 - }) 184 + ) 334 185 335 186 assert_receive {:ws_message, %{"type" => "ack", "id" => 710}}, 5_000 336 187 ··· 353 204 pulls_html("repoowner.test", "testrepo", datetime: "2026-03-01T00:00:00+00:00") 354 205 ) 355 206 356 - {:ok, _pid} = 357 - Atvouch.Tap.Socket.start_link( 358 - uri: "ws://localhost:#{tap_port}/channel", 359 - handler: Atvouch.TapHandler, 360 - password: "123", 361 - name: :"pull_same_ts_test_#{tap_port}" 362 - ) 207 + ws_pid = start_tap_socket(tap_port) 363 208 364 - assert_receive {:ws_connected, ws_pid}, 5_000 365 - 366 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 367 - "id" => 711, 368 - "type" => "record", 369 - "record" => %{ 370 - "action" => "create", 371 - "did" => "did:plc:author", 372 - "rev" => "3mgmqjki6sz2n", 373 - "collection" => "sh.tangled.repo.pull", 374 - "rkey" => "3pullsame", 375 - "record" => %{ 376 - "$type" => "sh.tangled.repo.pull", 377 - "title" => "PR at exact membership time", 378 - "createdAt" => "2026-03-19T10:00:00Z", 379 - "target" => %{ 380 - "repo" => repo_at_uri, 381 - "branch" => "main" 382 - } 383 - }, 384 - "cid" => "bafypullsame", 385 - "live" => true 386 - } 387 - }) 209 + Atvouch.Test.FakeTapServer.send_event( 210 + ws_pid, 211 + pull_record_event(711, "did:plc:author", repo_at_uri, 212 + rkey: "3pullsame", 213 + title: "PR at exact membership time" 214 + ) 215 + ) 388 216 389 217 assert_receive {:ws_message, %{"type" => "ack", "id" => 711}}, 5_000 390 218 ··· 405 233 pulls_html("repoowner.test", "testrepo") 406 234 ) 407 235 408 - {:ok, _pid} = 409 - Atvouch.Tap.Socket.start_link( 410 - uri: "ws://localhost:#{tap_port}/channel", 411 - handler: Atvouch.TapHandler, 412 - password: "123", 413 - name: :"pull_dedup_test_#{tap_port}" 414 - ) 415 - 416 - assert_receive {:ws_connected, ws_pid}, 5_000 236 + ws_pid = start_tap_socket(tap_port) 417 237 418 - pull_event = %{ 419 - "type" => "record", 420 - "record" => %{ 421 - "action" => "create", 422 - "did" => "did:plc:author", 423 - "rev" => "3mgmqjki6sz2n", 424 - "collection" => "sh.tangled.repo.pull", 425 - "rkey" => "3pull789", 426 - "record" => %{ 427 - "$type" => "sh.tangled.repo.pull", 428 - "title" => "Fix tests", 429 - "createdAt" => "2026-03-19T10:00:00Z", 430 - "target" => %{ 431 - "repo" => repo_at_uri, 432 - "branch" => "main" 433 - } 434 - }, 435 - "cid" => "bafypull3", 436 - "live" => true 437 - } 438 - } 238 + pull_event = 239 + pull_record_event(0, "did:plc:author", repo_at_uri, 240 + rkey: "3pull789", 241 + title: "Fix tests" 242 + ) 439 243 440 244 # First event 441 245 Atvouch.Test.FakeTapServer.send_event(ws_pid, Map.put(pull_event, "id", 702)) 442 246 assert_receive {:ws_message, %{"type" => "ack", "id" => 702}}, 10_000 443 247 assert_receive {:tangled_comment, _}, 10_000 444 248 445 - # Drain remaining messages 446 249 drain_messages() 447 250 448 251 # Second event for same repo ··· 475 278 pulls_html("repoowner.test", "testrepo") 476 279 ) 477 280 478 - {:ok, _pid} = 479 - Atvouch.Tap.Socket.start_link( 480 - uri: "ws://localhost:#{tap_port}/channel", 481 - handler: Atvouch.TapHandler, 482 - password: "123", 483 - name: :"pull_race_test_#{tap_port}" 281 + ws_pid = start_tap_socket(tap_port) 282 + 283 + Atvouch.Test.FakeTapServer.send_event( 284 + ws_pid, 285 + pull_record_event(720, "did:plc:author", repo_at_uri, 286 + rkey: "3pullrace", 287 + title: "Race condition PR" 484 288 ) 485 - 486 - assert_receive {:ws_connected, ws_pid}, 5_000 487 - 488 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 489 - "id" => 720, 490 - "type" => "record", 491 - "record" => %{ 492 - "action" => "create", 493 - "did" => "did:plc:author", 494 - "rev" => "3mgmqjki6sz2n", 495 - "collection" => "sh.tangled.repo.pull", 496 - "rkey" => "3pullrace", 497 - "record" => %{ 498 - "$type" => "sh.tangled.repo.pull", 499 - "title" => "Race condition PR", 500 - "createdAt" => "2026-03-19T10:00:00Z", 501 - "target" => %{ 502 - "repo" => repo_at_uri, 503 - "branch" => "main" 504 - } 505 - }, 506 - "cid" => "bafypullrace", 507 - "live" => true 508 - } 509 - }) 289 + ) 510 290 511 291 # Should process without crashing 512 292 assert_receive {:ws_message, %{"type" => "ack", "id" => 720}}, 10_000 ··· 523 303 repo_at_uri_1 = setup_vouch_graph() 524 304 525 305 # Set up second repo with its own membership 526 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:repoowner2", handle: "repoowner2.test"}) 306 + create_identity("did:plc:repoowner2", "repoowner2.test") 527 307 528 308 repo_at_uri_2 = "at://did:plc:repoowner2/sh.tangled.repo/3def456" 529 309 ··· 555 335 pulls_html("repoowner2.test", "otherrepo") 556 336 ) 557 337 558 - {:ok, _pid} = 559 - Atvouch.Tap.Socket.start_link( 560 - uri: "ws://localhost:#{tap_port}/channel", 561 - handler: Atvouch.TapHandler, 562 - password: "123", 563 - name: :"pull_multi_repo_test_#{tap_port}" 564 - ) 565 - 566 - assert_receive {:ws_connected, ws_pid}, 5_000 338 + ws_pid = start_tap_socket(tap_port) 567 339 568 340 # Send pull event for first repo 569 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 570 - "id" => 730, 571 - "type" => "record", 572 - "record" => %{ 573 - "action" => "create", 574 - "did" => "did:plc:author", 575 - "rev" => "3mgmqjki6sz2n", 576 - "collection" => "sh.tangled.repo.pull", 577 - "rkey" => "3pullmulti1", 578 - "record" => %{ 579 - "$type" => "sh.tangled.repo.pull", 580 - "title" => "PR on repo 1", 581 - "createdAt" => "2026-03-19T10:00:00Z", 582 - "target" => %{ 583 - "repo" => repo_at_uri_1, 584 - "branch" => "main" 585 - } 586 - }, 587 - "cid" => "bafypullmulti1", 588 - "live" => true 589 - } 590 - }) 341 + Atvouch.Test.FakeTapServer.send_event( 342 + ws_pid, 343 + pull_record_event(730, "did:plc:author", repo_at_uri_1, 344 + rkey: "3pullmulti1", 345 + title: "PR on repo 1" 346 + ) 347 + ) 591 348 592 349 assert_receive {:ws_message, %{"type" => "ack", "id" => 730}}, 10_000 593 350 assert_receive {:tangled_comment, comment1}, 10_000 ··· 595 352 assert comment1.rkey == "testrepo" 596 353 assert comment1.number == 1 597 354 598 - # Drain 599 355 drain_messages() 600 356 601 357 # Send pull event for second repo (same pull number #1) 602 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 603 - "id" => 731, 604 - "type" => "record", 605 - "record" => %{ 606 - "action" => "create", 607 - "did" => "did:plc:author", 608 - "rev" => "3mgmqjki6sz2n", 609 - "collection" => "sh.tangled.repo.pull", 610 - "rkey" => "3pullmulti2", 611 - "record" => %{ 612 - "$type" => "sh.tangled.repo.pull", 613 - "title" => "PR on repo 2", 614 - "createdAt" => "2026-03-19T10:00:00Z", 615 - "target" => %{ 616 - "repo" => repo_at_uri_2, 617 - "branch" => "main" 618 - } 619 - }, 620 - "cid" => "bafypullmulti2", 621 - "live" => true 622 - } 623 - }) 358 + Atvouch.Test.FakeTapServer.send_event( 359 + ws_pid, 360 + pull_record_event(731, "did:plc:author", repo_at_uri_2, 361 + rkey: "3pullmulti2", 362 + title: "PR on repo 2" 363 + ) 364 + ) 624 365 625 366 assert_receive {:ws_message, %{"type" => "ack", "id" => 731}}, 10_000 626 367 assert_receive {:tangled_comment, comment2}, 10_000 ··· 631 372 # Both repos should have their own dedup records 632 373 assert Atvouch.BotComment.exists?(repo_at_uri_1, 1) 633 374 assert Atvouch.BotComment.exists?(repo_at_uri_2, 1) 634 - end 635 - 636 - defp drain_messages do 637 - receive do 638 - _ -> drain_messages() 639 - after 640 - 100 -> :ok 641 - end 642 375 end 643 376 end
+10 -38
appview/test/atvouch/tangled/client_test.exs
··· 1 1 defmodule Atvouch.Tangled.ClientTest do 2 2 use ExUnit.Case 3 + import Atvouch.Test.Helpers 3 4 4 5 alias Atvouch.Tangled.Client 5 6 6 7 setup do 7 - # Start Tangled server 8 - {tangled_pid, tangled_port, state_agent} = 9 - Atvouch.Test.FakeTangledServer.start(self()) 10 - 11 - # Start PDS server with callback pointing to tangled 12 - {pds_pid, pds_port} = 13 - Atvouch.Test.FakePdsServer.start(self(), 14 - expected_username: "bot.test", 15 - expected_password: "test-password", 16 - callback_url: "http://127.0.0.1:#{tangled_port}/oauth/callback?code=test&state=test" 17 - ) 18 - 19 - # Set PDS URL on tangled 20 - Atvouch.Test.FakeTangledServer.set_pds_url(state_agent, "http://127.0.0.1:#{pds_port}") 21 - 22 - # Start session GenServer 23 - {:ok, session_pid} = 24 - Atvouch.Tangled.Session.start_link( 25 - handle: "bot.test", 26 - password: "test-password", 27 - tangled_url: "http://127.0.0.1:#{tangled_port}", 28 - name: :"client_test_session_#{tangled_port}" 29 - ) 8 + servers = start_tangled_pds_session(self(), 9 + session_name: :"client_test_session_#{System.unique_integer([:positive])}" 10 + ) 30 11 31 12 on_exit(fn -> 32 - for pid <- [pds_pid, tangled_pid] do 33 - try do 34 - Supervisor.stop(pid, :normal, 1_000) 35 - catch 36 - :exit, _ -> :ok 37 - end 38 - end 13 + for pid <- [servers.pds_pid, servers.tangled_pid], 14 + do: safe_stop_supervisor(pid) 39 15 40 - try do 41 - GenServer.stop(session_pid, :normal, 1_000) 42 - catch 43 - :exit, _ -> :ok 44 - end 16 + safe_stop_genserver(servers.session_pid) 45 17 end) 46 18 47 19 {:ok, 48 - tangled_port: tangled_port, 49 - session: session_pid, 50 - state_agent: state_agent} 20 + tangled_port: servers.tangled_port, 21 + session: servers.session_pid, 22 + state_agent: servers.state_agent} 51 23 end 52 24 53 25 test "successfully posts a comment", %{session: session} do
+5 -30
appview/test/atvouch/tangled/session_test.exs
··· 1 1 defmodule Atvouch.Tangled.SessionTest do 2 2 use ExUnit.Case 3 + import Atvouch.Test.Helpers 3 4 4 5 alias Atvouch.Tangled.Session 5 6 6 7 setup do 7 - # Start Tangled server (PDS URL will be set after PDS starts) 8 - {tangled_pid, tangled_port, state_agent} = 9 - Atvouch.Test.FakeTangledServer.start(self()) 10 - 11 - # Start PDS server with callback URL pointing to tangled 12 - {pds_pid, pds_port} = 13 - Atvouch.Test.FakePdsServer.start(self(), 14 - expected_username: "bot.test", 15 - expected_password: "test-password", 16 - callback_url: "http://127.0.0.1:#{tangled_port}/oauth/callback?code=test-code&state=test-state" 17 - ) 18 - 19 - # Now set the PDS URL on the tangled server 20 - Atvouch.Test.FakeTangledServer.set_pds_url(state_agent, "http://127.0.0.1:#{pds_port}") 8 + servers = start_tangled_pds(self()) 21 9 22 10 on_exit(fn -> 23 - for pid <- [pds_pid, tangled_pid] do 24 - try do 25 - Supervisor.stop(pid, :normal, 1_000) 26 - catch 27 - :exit, _ -> :ok 28 - end 29 - end 11 + for pid <- [servers.pds_pid, servers.tangled_pid], 12 + do: safe_stop_supervisor(pid) 30 13 end) 31 14 32 - {:ok, tangled_port: tangled_port} 15 + {:ok, tangled_port: servers.tangled_port} 33 16 end 34 17 35 18 test "successful login returns cookies", %{tangled_port: tangled_port} do ··· 105 88 106 89 # GenServer should still be alive 107 90 assert Process.alive?(pid) 108 - end 109 - 110 - defp drain_messages do 111 - receive do 112 - _ -> drain_messages() 113 - after 114 - 50 -> :ok 115 - end 116 91 end 117 92 end
+2 -5
appview/test/atvouch/tap/socket_test.exs
··· 1 1 defmodule Atvouch.Tap.SocketTest do 2 2 use ExUnit.Case 3 + import Atvouch.Test.Helpers 3 4 4 5 defmodule TestHandler do 5 6 @behaviour Atvouch.Tap.Handler ··· 28 29 {server_pid, port} = Atvouch.Test.FakeTapServer.start(self()) 29 30 30 31 on_exit(fn -> 31 - try do 32 - Supervisor.stop(server_pid, :normal, 1_000) 33 - catch 34 - :exit, _ -> :ok 35 - end 32 + safe_stop_supervisor(server_pid) 36 33 end) 37 34 38 35 {:ok, port: port}
+130 -487
appview/test/atvouch/tap_handler_test.exs
··· 1 1 defmodule Atvouch.TapHandlerTest do 2 2 use ExUnit.Case 3 + import Atvouch.Test.Helpers 3 4 4 5 setup do 5 - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 6 - Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 6 + checkout_sandbox() 7 7 8 8 Process.register(self(), :tap_handler_test) 9 9 {server_pid, port} = Atvouch.Test.FakeTapServer.start(self()) 10 10 11 11 # Use fake atproto module for repo record resolution 12 - prev_atproto = Application.get_env(:atvouch, :atproto_module) 13 - Application.put_env(:atvouch, :atproto_module, Atvouch.Test.FakeAtproto) 12 + prev_atproto = set_env(:atproto_module, Atvouch.Test.FakeAtproto) 14 13 {:ok, _} = Atvouch.Test.FakeAtproto.start() 15 14 16 15 on_exit(fn -> 17 - if prev_atproto do 18 - Application.put_env(:atvouch, :atproto_module, prev_atproto) 19 - else 20 - Application.delete_env(:atvouch, :atproto_module) 21 - end 22 - 23 - try do 24 - Supervisor.stop(server_pid, :normal, 1_000) 25 - catch 26 - :exit, _ -> :ok 27 - end 16 + restore_env(:atproto_module, prev_atproto) 17 + safe_stop_supervisor(server_pid) 28 18 end) 29 19 30 20 {:ok, port: port} 31 21 end 32 22 33 23 test "creates a vouch from a tap record event", %{port: port} do 34 - {:ok, _pid} = 35 - Atvouch.Tap.Socket.start_link( 36 - uri: "ws://localhost:#{port}/channel", 37 - handler: Atvouch.TapHandler, 38 - password: "123", 39 - name: :"tap_handler_test_#{port}" 40 - ) 41 - 42 - assert_receive {:ws_connected, ws_pid}, 5_000 24 + ws_pid = start_tap_socket(port) 43 25 44 26 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 45 27 target_did = "did:plc:wamidydbgu3u6fk3yckaglnz" 46 28 47 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 48 - "id" => 260, 49 - "type" => "record", 50 - "record" => %{ 51 - "action" => "create", 52 - "did" => creator_did, 53 - "rev" => "3mgmqjki6sz2n", 54 - "collection" => "dev.atvouch.graph.vouch", 55 - "rkey" => target_did, 56 - "record" => %{ 57 - "$type" => "dev.atvouch.graph.vouch", 58 - "createdAt" => "2026-03-09T10:53:26.922Z", 59 - "subject" => target_did 60 - }, 61 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 62 - "live" => false 63 - } 64 - }) 29 + Atvouch.Test.FakeTapServer.send_event( 30 + ws_pid, 31 + vouch_record_event(260, creator_did, target_did) 32 + ) 65 33 66 34 # Wait for the handler to process (ack means it succeeded) 67 35 assert_receive {:ws_message, %{"type" => "ack", "id" => 260}}, 5_000 ··· 93 61 assert target.status == nil 94 62 95 63 # Identity event fills in handle/status for the creator 96 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 97 - "id" => 261, 98 - "type" => "identity", 99 - "identity" => %{ 100 - "did" => creator_did, 101 - "handle" => "cpzv.bsky.social", 102 - "is_active" => true, 103 - "status" => "active" 104 - } 105 - }) 64 + Atvouch.Test.FakeTapServer.send_event( 65 + ws_pid, 66 + identity_event(261, creator_did, handle: "cpzv.bsky.social") 67 + ) 106 68 107 69 assert_receive {:ws_message, %{"type" => "ack", "id" => 261}}, 5_000 108 70 ··· 113 75 end 114 76 115 77 test "creates and updates an identity from tap identity events", %{port: port} do 116 - {:ok, _pid} = 117 - Atvouch.Tap.Socket.start_link( 118 - uri: "ws://localhost:#{port}/channel", 119 - handler: Atvouch.TapHandler, 120 - password: "123", 121 - name: :"tap_handler_identity_test_#{port}" 122 - ) 123 - 124 - assert_receive {:ws_connected, ws_pid}, 5_000 78 + ws_pid = start_tap_socket(port) 125 79 126 80 did = "did:plc:ztmnhdydzzivdmoezxcyrsof" 127 81 128 82 # First event: create identity 129 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 130 - "id" => 202, 131 - "type" => "identity", 132 - "identity" => %{ 133 - "did" => did, 134 - "handle" => "ioi-xd.net", 135 - "is_active" => true, 136 - "status" => "active" 137 - } 138 - }) 83 + Atvouch.Test.FakeTapServer.send_event( 84 + ws_pid, 85 + identity_event(202, did, handle: "ioi-xd.net") 86 + ) 139 87 140 88 assert_receive {:ws_message, %{"type" => "ack", "id" => 202}}, 5_000 141 89 ··· 147 95 assert identity.status == "active" 148 96 149 97 # Second event: handle change 150 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 151 - "id" => 203, 152 - "type" => "identity", 153 - "identity" => %{ 154 - "did" => did, 155 - "handle" => "new-handle.bsky.social", 156 - "is_active" => true, 157 - "status" => "active" 158 - } 159 - }) 98 + Atvouch.Test.FakeTapServer.send_event( 99 + ws_pid, 100 + identity_event(203, did, handle: "new-handle.bsky.social") 101 + ) 160 102 161 103 assert_receive {:ws_message, %{"type" => "ack", "id" => 203}}, 5_000 162 104 ··· 166 108 end 167 109 168 110 test "deletes a vouch from a tap record delete event", %{port: port} do 169 - {:ok, _pid} = 170 - Atvouch.Tap.Socket.start_link( 171 - uri: "ws://localhost:#{port}/channel", 172 - handler: Atvouch.TapHandler, 173 - password: "123", 174 - name: :"tap_handler_delete_test_#{port}" 175 - ) 176 - 177 - assert_receive {:ws_connected, ws_pid}, 5_000 111 + ws_pid = start_tap_socket(port) 178 112 179 113 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 180 114 target_did = "did:plc:wamidydbgu3u6fk3yckaglnz" 181 115 182 116 # First create a vouch 183 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 184 - "id" => 300, 185 - "type" => "record", 186 - "record" => %{ 187 - "action" => "create", 188 - "did" => creator_did, 189 - "rev" => "3mgmqjki6sz2n", 190 - "collection" => "dev.atvouch.graph.vouch", 191 - "rkey" => target_did, 192 - "record" => %{ 193 - "$type" => "dev.atvouch.graph.vouch", 194 - "createdAt" => "2026-03-09T10:53:26.922Z", 195 - "subject" => target_did 196 - }, 197 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 198 - "live" => true 199 - } 200 - }) 117 + Atvouch.Test.FakeTapServer.send_event( 118 + ws_pid, 119 + vouch_record_event(300, creator_did, target_did, live: true) 120 + ) 201 121 202 122 assert_receive {:ws_message, %{"type" => "ack", "id" => 300}}, 5_000 203 123 ··· 207 127 assert vouch.live == true 208 128 209 129 # Now delete it 210 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 211 - "id" => 301, 212 - "type" => "record", 213 - "record" => %{ 214 - "action" => "delete", 215 - "did" => creator_did, 216 - "rev" => "3mgmqjki6sz2o", 217 - "collection" => "dev.atvouch.graph.vouch", 218 - "rkey" => target_did 219 - } 220 - }) 130 + Atvouch.Test.FakeTapServer.send_event( 131 + ws_pid, 132 + vouch_record_event(301, creator_did, target_did, action: "delete", rev: "3mgmqjki6sz2o") 133 + ) 221 134 222 135 assert_receive {:ws_message, %{"type" => "ack", "id" => 301}}, 5_000 223 136 ··· 225 138 end 226 139 227 140 test "rejects record where rkey does not match subject", %{port: port} do 228 - {:ok, _pid} = 229 - Atvouch.Tap.Socket.start_link( 230 - uri: "ws://localhost:#{port}/channel", 231 - handler: Atvouch.TapHandler, 232 - password: "123", 233 - name: :"tap_handler_rkey_mismatch_test_#{port}" 234 - ) 235 - 236 - assert_receive {:ws_connected, ws_pid}, 5_000 141 + ws_pid = start_tap_socket(port) 237 142 238 143 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 239 144 target_did = "did:plc:wamidydbgu3u6fk3yckaglnz" 240 145 wrong_rkey = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaaa" 241 146 242 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 147 + # Build a vouch event with mismatched rkey and subject 148 + event = %{ 243 149 "id" => 500, 244 150 "type" => "record", 245 151 "record" => %{ ··· 256 162 "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 257 163 "live" => false 258 164 } 259 - }) 165 + } 166 + 167 + Atvouch.Test.FakeTapServer.send_event(ws_pid, event) 260 168 261 - # Should still be ACKed 262 169 assert_receive {:ws_message, %{"type" => "ack", "id" => 500}}, 5_000 263 170 264 171 # But no vouch should be created 265 - at_uri = "at://#{creator_did}/dev.atvouch.graph.vouch/#{wrong_rkey}" 266 - assert Atvouch.Vouch.one(at_uri) == nil 267 - 268 - # Also no vouch with the subject as rkey 269 - at_uri2 = "at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}" 270 - assert Atvouch.Vouch.one(at_uri2) == nil 172 + assert Atvouch.Vouch.one("at://#{creator_did}/dev.atvouch.graph.vouch/#{wrong_rkey}") == nil 173 + assert Atvouch.Vouch.one("at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}") == nil 271 174 272 175 # No identities should have been created either 273 176 assert Atvouch.Identity.one(creator_did) == nil ··· 275 178 end 276 179 277 180 test "rejects record with invalid DID in subject", %{port: port} do 278 - {:ok, _pid} = 279 - Atvouch.Tap.Socket.start_link( 280 - uri: "ws://localhost:#{port}/channel", 281 - handler: Atvouch.TapHandler, 282 - password: "123", 283 - name: :"tap_handler_invalid_did_test_#{port}" 284 - ) 285 - 286 - assert_receive {:ws_connected, ws_pid}, 5_000 181 + ws_pid = start_tap_socket(port) 287 182 288 183 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 289 184 invalid_subject = "not-a-valid-did" 290 185 291 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 292 - "id" => 501, 293 - "type" => "record", 294 - "record" => %{ 295 - "action" => "create", 296 - "did" => creator_did, 297 - "rev" => "3mgmqjki6sz2n", 298 - "collection" => "dev.atvouch.graph.vouch", 299 - "rkey" => invalid_subject, 300 - "record" => %{ 301 - "$type" => "dev.atvouch.graph.vouch", 302 - "createdAt" => "2026-03-09T10:53:26.922Z", 303 - "subject" => invalid_subject 304 - }, 305 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 306 - "live" => false 307 - } 308 - }) 186 + Atvouch.Test.FakeTapServer.send_event( 187 + ws_pid, 188 + vouch_record_event(501, creator_did, invalid_subject) 189 + ) 309 190 310 - # Should still be ACKed 311 191 assert_receive {:ws_message, %{"type" => "ack", "id" => 501}}, 5_000 312 192 313 - # But no vouch should be created 314 - at_uri = "at://#{creator_did}/dev.atvouch.graph.vouch/#{invalid_subject}" 315 - assert Atvouch.Vouch.one(at_uri) == nil 193 + assert Atvouch.Vouch.one("at://#{creator_did}/dev.atvouch.graph.vouch/#{invalid_subject}") == nil 316 194 end 317 195 318 196 test "rejects record with invalid createdAt timestamp", %{port: port} do 319 - {:ok, _pid} = 320 - Atvouch.Tap.Socket.start_link( 321 - uri: "ws://localhost:#{port}/channel", 322 - handler: Atvouch.TapHandler, 323 - password: "123", 324 - name: :"tap_handler_invalid_timestamp_test_#{port}" 325 - ) 326 - 327 - assert_receive {:ws_connected, ws_pid}, 5_000 197 + ws_pid = start_tap_socket(port) 328 198 329 199 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 330 200 target_did = "did:plc:wamidydbgu3u6fk3yckaglnz" 331 201 332 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 333 - "id" => 502, 334 - "type" => "record", 335 - "record" => %{ 336 - "action" => "create", 337 - "did" => creator_did, 338 - "rev" => "3mgmqjki6sz2n", 339 - "collection" => "dev.atvouch.graph.vouch", 340 - "rkey" => target_did, 341 - "record" => %{ 342 - "$type" => "dev.atvouch.graph.vouch", 343 - "createdAt" => "not-a-timestamp", 344 - "subject" => target_did 345 - }, 346 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 347 - "live" => false 348 - } 349 - }) 202 + Atvouch.Test.FakeTapServer.send_event( 203 + ws_pid, 204 + vouch_record_event(502, creator_did, target_did, created_at: "not-a-timestamp") 205 + ) 350 206 351 - # Should still be ACKed 352 207 assert_receive {:ws_message, %{"type" => "ack", "id" => 502}}, 5_000 353 208 354 - # But no vouch should be created 355 - at_uri = "at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}" 356 - assert Atvouch.Vouch.one(at_uri) == nil 209 + assert Atvouch.Vouch.one("at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}") == nil 357 210 end 358 211 359 212 test "rejects record with missing subject field", %{port: port} do 360 - {:ok, _pid} = 361 - Atvouch.Tap.Socket.start_link( 362 - uri: "ws://localhost:#{port}/channel", 363 - handler: Atvouch.TapHandler, 364 - password: "123", 365 - name: :"tap_handler_missing_subject_test_#{port}" 366 - ) 367 - 368 - assert_receive {:ws_connected, ws_pid}, 5_000 213 + ws_pid = start_tap_socket(port) 369 214 370 215 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 371 216 372 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 217 + # Build manually since the helper always sets subject = rkey 218 + event = %{ 373 219 "id" => 503, 374 220 "type" => "record", 375 221 "record" => %{ ··· 385 231 "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 386 232 "live" => false 387 233 } 388 - }) 234 + } 235 + 236 + Atvouch.Test.FakeTapServer.send_event(ws_pid, event) 389 237 390 - # Should still be ACKed 391 238 assert_receive {:ws_message, %{"type" => "ack", "id" => 503}}, 5_000 392 239 393 - # No vouch created 394 - at_uri = "at://#{creator_did}/dev.atvouch.graph.vouch/some-rkey" 395 - assert Atvouch.Vouch.one(at_uri) == nil 240 + assert Atvouch.Vouch.one("at://#{creator_did}/dev.atvouch.graph.vouch/some-rkey") == nil 396 241 end 397 242 398 243 test "rejects record where subject equals creator did (self-vouch)", %{port: port} do 399 - {:ok, _pid} = 400 - Atvouch.Tap.Socket.start_link( 401 - uri: "ws://localhost:#{port}/channel", 402 - handler: Atvouch.TapHandler, 403 - password: "123", 404 - name: :"tap_handler_self_vouch_test_#{port}" 405 - ) 406 - 407 - assert_receive {:ws_connected, ws_pid}, 5_000 244 + ws_pid = start_tap_socket(port) 408 245 409 246 self_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 410 247 411 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 412 - "id" => 504, 413 - "type" => "record", 414 - "record" => %{ 415 - "action" => "create", 416 - "did" => self_did, 417 - "rev" => "3mgmqjki6sz2n", 418 - "collection" => "dev.atvouch.graph.vouch", 419 - "rkey" => self_did, 420 - "record" => %{ 421 - "$type" => "dev.atvouch.graph.vouch", 422 - "createdAt" => "2026-03-09T10:53:26.922Z", 423 - "subject" => self_did 424 - }, 425 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 426 - "live" => false 427 - } 428 - }) 248 + Atvouch.Test.FakeTapServer.send_event( 249 + ws_pid, 250 + vouch_record_event(504, self_did, self_did) 251 + ) 429 252 430 - # Should still be ACKed 431 253 assert_receive {:ws_message, %{"type" => "ack", "id" => 504}}, 5_000 432 254 433 - # No vouch created 434 - at_uri = "at://#{self_did}/dev.atvouch.graph.vouch/#{self_did}" 435 - assert Atvouch.Vouch.one(at_uri) == nil 255 + assert Atvouch.Vouch.one("at://#{self_did}/dev.atvouch.graph.vouch/#{self_did}") == nil 436 256 end 437 257 438 258 test "creates a membership from a tap record event", %{port: port} do 439 - {:ok, _pid} = 440 - Atvouch.Tap.Socket.start_link( 441 - uri: "ws://localhost:#{port}/channel", 442 - handler: Atvouch.TapHandler, 443 - password: "123", 444 - name: :"tap_handler_membership_create_test_#{port}" 445 - ) 446 - 447 - assert_receive {:ws_connected, ws_pid}, 5_000 259 + ws_pid = start_tap_socket(port) 448 260 449 261 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 450 262 repo_did = "did:plc:wamidydbgu3u6fk3yckaglnz" ··· 458 270 "value" => %{"name" => "my-cool-repo", "knot" => "knot.test", "createdAt" => "2026-03-01T00:00:00Z"} 459 271 }) 460 272 461 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 462 - "id" => 600, 463 - "type" => "record", 464 - "record" => %{ 465 - "action" => "create", 466 - "did" => source_did, 467 - "rev" => "3mgmqjki6sz2n", 468 - "collection" => "dev.atvouch.bot.membership", 469 - "rkey" => repo_rkey, 470 - "record" => %{ 471 - "$type" => "dev.atvouch.bot.membership", 472 - "repo" => repo_at_uri, 473 - "maintainers" => [maintainer1, maintainer2] 474 - }, 475 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 476 - "live" => false 477 - } 478 - }) 273 + Atvouch.Test.FakeTapServer.send_event( 274 + ws_pid, 275 + membership_record_event(600, source_did, repo_at_uri, [maintainer1, maintainer2]) 276 + ) 479 277 480 278 assert_receive {:ws_message, %{"type" => "ack", "id" => 600}}, 5_000 481 279 ··· 493 291 end 494 292 495 293 test "updates a membership's maintainers from a tap record update event", %{port: port} do 496 - {:ok, _pid} = 497 - Atvouch.Tap.Socket.start_link( 498 - uri: "ws://localhost:#{port}/channel", 499 - handler: Atvouch.TapHandler, 500 - password: "123", 501 - name: :"tap_handler_membership_update_test_#{port}" 502 - ) 503 - 504 - assert_receive {:ws_connected, ws_pid}, 5_000 294 + ws_pid = start_tap_socket(port) 505 295 506 296 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 507 297 repo_rkey = "3mgmqjki6sz2n" ··· 515 305 }) 516 306 517 307 # Create 518 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 519 - "id" => 610, 520 - "type" => "record", 521 - "record" => %{ 522 - "action" => "create", 523 - "did" => source_did, 524 - "rev" => "3mgmqjki6sz2n", 525 - "collection" => "dev.atvouch.bot.membership", 526 - "rkey" => repo_rkey, 527 - "record" => %{ 528 - "$type" => "dev.atvouch.bot.membership", 529 - "repo" => repo_at_uri, 530 - "maintainers" => [maintainer1, maintainer2] 531 - }, 532 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 533 - "live" => false 534 - } 535 - }) 308 + Atvouch.Test.FakeTapServer.send_event( 309 + ws_pid, 310 + membership_record_event(610, source_did, repo_at_uri, [maintainer1, maintainer2]) 311 + ) 536 312 537 313 assert_receive {:ws_message, %{"type" => "ack", "id" => 610}}, 5_000 538 314 assert Enum.sort(Atvouch.Membership.maintainers(repo_at_uri)) == Enum.sort([maintainer1, maintainer2]) 539 315 540 316 # Update: replace maintainers 541 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 542 - "id" => 611, 543 - "type" => "record", 544 - "record" => %{ 545 - "action" => "update", 546 - "did" => source_did, 547 - "rev" => "3mgmqjki6sz2o", 548 - "collection" => "dev.atvouch.bot.membership", 549 - "rkey" => repo_rkey, 550 - "record" => %{ 551 - "$type" => "dev.atvouch.bot.membership", 552 - "repo" => repo_at_uri, 553 - "maintainers" => [maintainer2, maintainer3] 554 - }, 555 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmjv", 556 - "live" => false 557 - } 558 - }) 317 + Atvouch.Test.FakeTapServer.send_event( 318 + ws_pid, 319 + membership_record_event(611, source_did, repo_at_uri, [maintainer2, maintainer3], 320 + action: "update", 321 + rev: "3mgmqjki6sz2o", 322 + cid: "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmjv" 323 + ) 324 + ) 559 325 560 326 assert_receive {:ws_message, %{"type" => "ack", "id" => 611}}, 5_000 561 327 assert Enum.sort(Atvouch.Membership.maintainers(repo_at_uri)) == Enum.sort([maintainer2, maintainer3]) 562 328 end 563 329 564 330 test "deletes a membership and its maintainers from a tap record delete event", %{port: port} do 565 - {:ok, _pid} = 566 - Atvouch.Tap.Socket.start_link( 567 - uri: "ws://localhost:#{port}/channel", 568 - handler: Atvouch.TapHandler, 569 - password: "123", 570 - name: :"tap_handler_membership_delete_test_#{port}" 571 - ) 572 - 573 - assert_receive {:ws_connected, ws_pid}, 5_000 331 + ws_pid = start_tap_socket(port) 574 332 575 333 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 576 334 repo_rkey = "3mgmqjki6sz2n" ··· 582 340 }) 583 341 584 342 # Create 585 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 586 - "id" => 620, 587 - "type" => "record", 588 - "record" => %{ 589 - "action" => "create", 590 - "did" => source_did, 591 - "rev" => "3mgmqjki6sz2n", 592 - "collection" => "dev.atvouch.bot.membership", 593 - "rkey" => repo_rkey, 594 - "record" => %{ 595 - "$type" => "dev.atvouch.bot.membership", 596 - "repo" => repo_at_uri, 597 - "maintainers" => [maintainer1] 598 - }, 599 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 600 - "live" => false 601 - } 602 - }) 343 + Atvouch.Test.FakeTapServer.send_event( 344 + ws_pid, 345 + membership_record_event(620, source_did, repo_at_uri, [maintainer1]) 346 + ) 603 347 604 348 assert_receive {:ws_message, %{"type" => "ack", "id" => 620}}, 5_000 605 349 assert Atvouch.Membership.one(repo_at_uri) != nil 606 350 607 351 # Delete 608 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 609 - "id" => 621, 610 - "type" => "record", 611 - "record" => %{ 612 - "action" => "delete", 613 - "did" => source_did, 614 - "rev" => "3mgmqjki6sz2o", 615 - "collection" => "dev.atvouch.bot.membership", 616 - "rkey" => repo_rkey 617 - } 618 - }) 352 + Atvouch.Test.FakeTapServer.send_event( 353 + ws_pid, 354 + membership_record_event(621, source_did, repo_at_uri, [], 355 + action: "delete", 356 + rev: "3mgmqjki6sz2o" 357 + ) 358 + ) 619 359 620 360 assert_receive {:ws_message, %{"type" => "ack", "id" => 621}}, 5_000 621 361 assert Atvouch.Membership.one(repo_at_uri) == nil ··· 623 363 end 624 364 625 365 test "rejects membership where rkey does not match repo rkey", %{port: port} do 626 - {:ok, _pid} = 627 - Atvouch.Tap.Socket.start_link( 628 - uri: "ws://localhost:#{port}/channel", 629 - handler: Atvouch.TapHandler, 630 - password: "123", 631 - name: :"tap_handler_membership_rkey_mismatch_test_#{port}" 632 - ) 633 - 634 - assert_receive {:ws_connected, ws_pid}, 5_000 366 + ws_pid = start_tap_socket(port) 635 367 636 368 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 637 369 repo_at_uri = "at://did:plc:wamidydbgu3u6fk3yckaglnz/sh.tangled.repo/3mgmqjki6sz2n" 638 - wrong_rkey = "wrong-rkey" 639 370 640 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 641 - "id" => 630, 642 - "type" => "record", 643 - "record" => %{ 644 - "action" => "create", 645 - "did" => source_did, 646 - "rev" => "3mgmqjki6sz2n", 647 - "collection" => "dev.atvouch.bot.membership", 648 - "rkey" => wrong_rkey, 649 - "record" => %{ 650 - "$type" => "dev.atvouch.bot.membership", 651 - "repo" => repo_at_uri, 652 - "maintainers" => ["did:plc:aaaaaaaaaaaaaaaaaaaaaaaaa"] 653 - }, 654 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 655 - "live" => false 656 - } 657 - }) 371 + Atvouch.Test.FakeTapServer.send_event( 372 + ws_pid, 373 + membership_record_event(630, source_did, repo_at_uri, ["did:plc:aaaaaaaaaaaaaaaaaaaaaaaaa"], 374 + rkey: "wrong-rkey" 375 + ) 376 + ) 658 377 659 378 assert_receive {:ws_message, %{"type" => "ack", "id" => 630}}, 5_000 660 379 assert Atvouch.Membership.one(repo_at_uri) == nil 661 380 end 662 381 663 382 test "rejects membership with invalid repo format", %{port: port} do 664 - {:ok, _pid} = 665 - Atvouch.Tap.Socket.start_link( 666 - uri: "ws://localhost:#{port}/channel", 667 - handler: Atvouch.TapHandler, 668 - password: "123", 669 - name: :"tap_handler_membership_invalid_repo_test_#{port}" 670 - ) 671 - 672 - assert_receive {:ws_connected, ws_pid}, 5_000 383 + ws_pid = start_tap_socket(port) 673 384 674 385 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 675 386 676 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 387 + # Build manually since the helper expects a valid AT URI to extract rkey 388 + event = %{ 677 389 "id" => 640, 678 390 "type" => "record", 679 391 "record" => %{ ··· 690 402 "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 691 403 "live" => false 692 404 } 693 - }) 405 + } 406 + 407 + Atvouch.Test.FakeTapServer.send_event(ws_pid, event) 694 408 695 409 assert_receive {:ws_message, %{"type" => "ack", "id" => 640}}, 5_000 696 410 # No membership should be created due to lexicon validation failure 697 411 end 698 412 699 413 test "rejects membership when referenced repo record does not exist", %{port: port} do 700 - {:ok, _pid} = 701 - Atvouch.Tap.Socket.start_link( 702 - uri: "ws://localhost:#{port}/channel", 703 - handler: Atvouch.TapHandler, 704 - password: "123", 705 - name: :"tap_handler_membership_no_repo_test_#{port}" 706 - ) 707 - 708 - assert_receive {:ws_connected, ws_pid}, 5_000 414 + ws_pid = start_tap_socket(port) 709 415 710 416 source_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 711 417 repo_did = "did:plc:wamidydbgu3u6fk3yckaglnz" ··· 713 419 repo_at_uri = "at://#{repo_did}/sh.tangled.repo/#{repo_rkey}" 714 420 715 421 # Do NOT register a fake record — the repo doesn't exist 716 - # FakeAtproto returns 404 by default for unknown records 717 - 718 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 719 - "id" => 650, 720 - "type" => "record", 721 - "record" => %{ 722 - "action" => "create", 723 - "did" => source_did, 724 - "rev" => "3mgmqjki6sz2n", 725 - "collection" => "dev.atvouch.bot.membership", 726 - "rkey" => repo_rkey, 727 - "record" => %{ 728 - "$type" => "dev.atvouch.bot.membership", 729 - "repo" => repo_at_uri, 730 - "maintainers" => ["did:plc:aaaaaaaaaaaaaaaaaaaaaaaaa"] 731 - }, 732 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 733 - "live" => false 734 - } 735 - }) 422 + Atvouch.Test.FakeTapServer.send_event( 423 + ws_pid, 424 + membership_record_event(650, source_did, repo_at_uri, ["did:plc:aaaaaaaaaaaaaaaaaaaaaaaaa"]) 425 + ) 736 426 737 427 assert_receive {:ws_message, %{"type" => "ack", "id" => 650}}, 5_000 738 428 739 - # No membership should be created — the repo record doesn't exist 740 429 assert Atvouch.Membership.one(repo_at_uri) == nil 741 430 end 742 431 743 432 test "processes pull record with lexicon-canonical target.repo format", %{port: port} do 744 - {:ok, _pid} = 745 - Atvouch.Tap.Socket.start_link( 746 - uri: "ws://localhost:#{port}/channel", 747 - handler: Atvouch.TapHandler, 748 - password: "123", 749 - name: :"tap_handler_pull_test_#{port}" 750 - ) 751 - 752 - assert_receive {:ws_connected, ws_pid}, 5_000 433 + ws_pid = start_tap_socket(port) 753 434 754 435 author_did = "did:plc:jjiv56ot7d6sgethto3jr3r5" 755 436 repo_at_uri = "at://did:plc:nhyitepp3u4u6fcfboegzcjw/sh.tangled.repo/3lkqb2xfney22" ··· 784 465 end 785 466 786 467 test "skips pull record with missing target repo field", %{port: port} do 787 - {:ok, _pid} = 788 - Atvouch.Tap.Socket.start_link( 789 - uri: "ws://localhost:#{port}/channel", 790 - handler: Atvouch.TapHandler, 791 - password: "123", 792 - name: :"tap_handler_pull_missing_target_test_#{port}" 793 - ) 794 - 795 - assert_receive {:ws_connected, ws_pid}, 5_000 468 + ws_pid = start_tap_socket(port) 796 469 797 470 # Send a pull record with no target field at all 798 471 Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ ··· 823 496 end 824 497 825 498 test "updates a vouch timestamp from a tap record update event", %{port: port} do 826 - {:ok, _pid} = 827 - Atvouch.Tap.Socket.start_link( 828 - uri: "ws://localhost:#{port}/channel", 829 - handler: Atvouch.TapHandler, 830 - password: "123", 831 - name: :"tap_handler_update_test_#{port}" 832 - ) 833 - 834 - assert_receive {:ws_connected, ws_pid}, 5_000 499 + ws_pid = start_tap_socket(port) 835 500 836 501 creator_did = "did:plc:cpzv5kdrtinsnj5rsblsttz6" 837 502 target_did = "did:plc:wamidydbgu3u6fk3yckaglnz" 838 503 839 504 # Create a vouch 840 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 841 - "id" => 400, 842 - "type" => "record", 843 - "record" => %{ 844 - "action" => "create", 845 - "did" => creator_did, 846 - "rev" => "3mgmqjki6sz2n", 847 - "collection" => "dev.atvouch.graph.vouch", 848 - "rkey" => target_did, 849 - "record" => %{ 850 - "$type" => "dev.atvouch.graph.vouch", 851 - "createdAt" => "2026-03-09T10:53:26.922Z", 852 - "subject" => target_did 853 - }, 854 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju", 855 - "live" => true 856 - } 857 - }) 505 + Atvouch.Test.FakeTapServer.send_event( 506 + ws_pid, 507 + vouch_record_event(400, creator_did, target_did, live: true) 508 + ) 858 509 859 510 assert_receive {:ws_message, %{"type" => "ack", "id" => 400}}, 5_000 860 511 ··· 866 517 assert vouch.remote_updated_timestamp == false 867 518 868 519 # Update with a different timestamp 869 - Atvouch.Test.FakeTapServer.send_event(ws_pid, %{ 870 - "id" => 401, 871 - "type" => "record", 872 - "record" => %{ 873 - "action" => "update", 874 - "did" => creator_did, 875 - "rev" => "3mgmqjki6sz2o", 876 - "collection" => "dev.atvouch.graph.vouch", 877 - "rkey" => target_did, 878 - "record" => %{ 879 - "$type" => "dev.atvouch.graph.vouch", 880 - "createdAt" => "2026-03-10T05:00:00.000Z", 881 - "subject" => target_did 882 - }, 883 - "cid" => "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmjv", 884 - "live" => true 885 - } 886 - }) 520 + Atvouch.Test.FakeTapServer.send_event( 521 + ws_pid, 522 + vouch_record_event(401, creator_did, target_did, 523 + action: "update", 524 + created_at: "2026-03-10T05:00:00.000Z", 525 + rev: "3mgmqjki6sz2o", 526 + cid: "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmjv", 527 + live: true 528 + ) 529 + ) 887 530 888 531 assert_receive {:ws_message, %{"type" => "ack", "id" => 401}}, 5_000 889 532
+13 -29
appview/test/atvouch/xrpc_get_routes_test.exs
··· 2 2 use ExUnit.Case 3 3 import Plug.Test 4 4 import Plug.Conn 5 + import Atvouch.Test.Helpers 5 6 6 7 @opts Atvouch.Router.init([]) 7 8 8 9 setup do 9 - Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 10 - Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 10 + checkout_sandbox() 11 11 12 - {_server_pid, auth_port} = Atvouch.Test.FakeAuthServer.start() 13 - auth_url = "http://127.0.0.1:#{auth_port}" 14 - prev_auth_url = Application.get_env(:atvouch, :auth_url) 15 - Application.put_env(:atvouch, :auth_url, auth_url) 12 + {_auth_url, prev_auth_url} = setup_auth_server() 16 13 17 14 on_exit(fn -> 18 - Application.put_env(:atvouch, :auth_url, prev_auth_url) 15 + restore_env(:auth_url, prev_auth_url) 19 16 end) 20 17 21 18 # Create identities 22 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:alice", handle: "alice.test"}) 23 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:bob", handle: "bob.test"}) 24 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:carol", handle: "carol.test"}) 25 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:dave", handle: "dave.test"}) 26 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:eve", handle: "eve.test"}) 19 + for {did, handle} <- [ 20 + {"did:plc:alice", "alice.test"}, 21 + {"did:plc:bob", "bob.test"}, 22 + {"did:plc:carol", "carol.test"}, 23 + {"did:plc:dave", "dave.test"}, 24 + {"did:plc:eve", "eve.test"} 25 + ] do 26 + create_identity(did, handle) 27 + end 27 28 28 29 :ok 29 - end 30 - 31 - defp create_vouch(creator_did, target_did, created_at \\ "2026-03-01T00:00:00Z") do 32 - {:ok, _} = 33 - Atvouch.Vouch.create(%{ 34 - at_uri: "at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}", 35 - creator_did: creator_did, 36 - target_did: target_did, 37 - original_created_at: created_at, 38 - remote_created_at: created_at, 39 - at_cid: "bafytest#{:erlang.phash2({creator_did, target_did})}", 40 - live: true 41 - }) 42 30 end 43 31 44 32 defp get_routes(target_did, auth_did) do ··· 212 200 end 213 201 214 202 test "shorter path subsumes longer path through same intermediary" do 215 - # A->B, B->C gives path A->B->C 216 - # A->D, D->B, B->C gives path A->D->B->C 217 - # The 2-hop path through B should subsume the 3-hop path through D->B 218 203 create_vouch("did:plc:alice", "did:plc:bob") 219 204 create_vouch("did:plc:alice", "did:plc:dave") 220 205 create_vouch("did:plc:bob", "did:plc:carol") ··· 229 214 end 230 215 231 216 test "keeps paths with different intermediaries" do 232 - # A->B->C and A->D->C have different intermediaries, both kept 233 217 create_vouch("did:plc:alice", "did:plc:bob") 234 218 create_vouch("did:plc:alice", "did:plc:dave") 235 219 create_vouch("did:plc:bob", "did:plc:carol")
+38 -90
appview/test/atvouch/xrpc_vouches_test.exs
··· 2 2 use ExUnit.Case 3 3 import Plug.Test 4 4 import Plug.Conn 5 + import Atvouch.Test.Helpers 5 6 6 7 @opts Atvouch.Router.init([]) 7 8 8 9 setup do 9 - Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 10 - Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 10 + checkout_sandbox() 11 11 12 - {_server_pid, auth_port} = Atvouch.Test.FakeAuthServer.start() 13 - auth_url = "http://127.0.0.1:#{auth_port}" 14 - prev_auth_url = Application.get_env(:atvouch, :auth_url) 15 - Application.put_env(:atvouch, :auth_url, auth_url) 12 + {_auth_url, prev_auth_url} = setup_auth_server() 16 13 17 14 on_exit(fn -> 18 - Application.put_env(:atvouch, :auth_url, prev_auth_url) 15 + restore_env(:auth_url, prev_auth_url) 19 16 end) 20 17 21 18 # Create test identities and vouches 22 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:alice", handle: "alice.test"}) 23 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:bob", handle: "bob.test"}) 24 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:carol", handle: "carol.test"}) 19 + create_identity("did:plc:alice", "alice.test") 20 + create_identity("did:plc:bob", "bob.test") 21 + create_identity("did:plc:carol", "carol.test") 25 22 26 23 # Alice vouched for Bob 27 - {:ok, _} = 28 - Atvouch.Vouch.create(%{ 29 - at_uri: "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:bob", 30 - creator_did: "did:plc:alice", 31 - target_did: "did:plc:bob", 32 - original_created_at: "2026-03-01T00:00:00Z", 33 - remote_created_at: "2026-03-01T00:00:00Z", 34 - at_cid: "bafyabc123", 35 - live: true 36 - }) 24 + create_vouch("did:plc:alice", "did:plc:bob", 25 + cid: "bafyabc123", 26 + created_at: "2026-03-01T00:00:00Z" 27 + ) 37 28 38 29 # Alice vouched for Carol 39 - {:ok, _} = 40 - Atvouch.Vouch.create(%{ 41 - at_uri: "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:carol", 42 - creator_did: "did:plc:alice", 43 - target_did: "did:plc:carol", 44 - original_created_at: "2026-03-02T00:00:00Z", 45 - remote_created_at: "2026-03-02T00:00:00Z", 46 - at_cid: "bafyabc456", 47 - live: true 48 - }) 30 + create_vouch("did:plc:alice", "did:plc:carol", 31 + cid: "bafyabc456", 32 + created_at: "2026-03-02T00:00:00Z" 33 + ) 49 34 50 35 # Bob vouched for Alice 51 - {:ok, _} = 52 - Atvouch.Vouch.create(%{ 53 - at_uri: "at://did:plc:bob/dev.atvouch.graph.vouch/did:plc:alice", 54 - creator_did: "did:plc:bob", 55 - target_did: "did:plc:alice", 56 - original_created_at: "2026-03-03T00:00:00Z", 57 - remote_created_at: "2026-03-03T00:00:00Z", 58 - at_cid: "bafyabc789", 59 - live: true 60 - }) 36 + create_vouch("did:plc:bob", "did:plc:alice", 37 + cid: "bafyabc789", 38 + created_at: "2026-03-03T00:00:00Z" 39 + ) 61 40 62 41 :ok 63 42 end ··· 131 110 132 111 test "cursor returns next page" do 133 112 # Add a third vouch for Alice so we can paginate with limit=2 134 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:dave", handle: "dave.test"}) 135 - 136 - {:ok, _} = 137 - Atvouch.Vouch.create(%{ 138 - at_uri: "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:dave", 139 - creator_did: "did:plc:alice", 140 - target_did: "did:plc:dave", 141 - original_created_at: "2026-03-04T00:00:00Z", 142 - remote_created_at: "2026-03-04T00:00:00Z", 143 - at_cid: "bafyabc111", 144 - live: true 145 - }) 113 + create_identity("did:plc:dave", "dave.test") 114 + create_vouch("did:plc:alice", "did:plc:dave", 115 + cid: "bafyabc111", 116 + created_at: "2026-03-04T00:00:00Z" 117 + ) 146 118 147 119 # Alice now has 3 vouches, fetch with limit=2 148 120 conn1 = ··· 277 249 end 278 250 279 251 test "returns total count" do 280 - # Bob has 2 vouches targeting him: from Alice (bob) and we check with bob who has 1 281 252 conn = 282 253 conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 283 254 |> put_req_header("authorization", "Bearer valid-token:did:plc:bob") ··· 289 260 end 290 261 291 262 test "paginates with limit" do 292 - # Alice has 1 remote vouch (from Bob), use bob who also has 1 293 - # To test pagination properly, we need a user with >1 remote vouches 294 - # Carol vouches for Bob too so bob has 2 targeting him 295 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:dave", handle: "dave.test"}) 296 - 297 - {:ok, _} = 298 - Atvouch.Vouch.create(%{ 299 - at_uri: "at://did:plc:carol/dev.atvouch.graph.vouch/did:plc:bob", 300 - creator_did: "did:plc:carol", 301 - target_did: "did:plc:bob", 302 - original_created_at: "2026-03-04T00:00:00Z", 303 - remote_created_at: "2026-03-04T00:00:00Z", 304 - at_cid: "bafyabc999", 305 - live: true 306 - }) 263 + create_identity("did:plc:dave", "dave.test") 264 + create_vouch("did:plc:carol", "did:plc:bob", 265 + cid: "bafyabc999", 266 + created_at: "2026-03-04T00:00:00Z" 267 + ) 307 268 308 269 conn = 309 270 conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches?limit=1") ··· 319 280 320 281 test "cursor returns next page" do 321 282 # Add 2 more remote vouches for Alice (she already has 1 from Bob) 322 - {:ok, _} = 323 - Atvouch.Vouch.create(%{ 324 - at_uri: "at://did:plc:carol/dev.atvouch.graph.vouch/did:plc:alice", 325 - creator_did: "did:plc:carol", 326 - target_did: "did:plc:alice", 327 - original_created_at: "2026-03-05T00:00:00Z", 328 - remote_created_at: "2026-03-05T00:00:00Z", 329 - at_cid: "bafyabc888", 330 - live: true 331 - }) 283 + create_vouch("did:plc:carol", "did:plc:alice", 284 + cid: "bafyabc888", 285 + created_at: "2026-03-05T00:00:00Z" 286 + ) 332 287 333 - {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:eve", handle: "eve.test"}) 334 - 335 - {:ok, _} = 336 - Atvouch.Vouch.create(%{ 337 - at_uri: "at://did:plc:eve/dev.atvouch.graph.vouch/did:plc:alice", 338 - creator_did: "did:plc:eve", 339 - target_did: "did:plc:alice", 340 - original_created_at: "2026-03-06T00:00:00Z", 341 - remote_created_at: "2026-03-06T00:00:00Z", 342 - at_cid: "bafyabc777", 343 - live: true 344 - }) 288 + create_identity("did:plc:eve", "eve.test") 289 + create_vouch("did:plc:eve", "did:plc:alice", 290 + cid: "bafyabc777", 291 + created_at: "2026-03-06T00:00:00Z" 292 + ) 345 293 346 294 # Alice now has 3 remote vouches, fetch with limit=2 347 295 # Ordered by original_created_at DESC: eve (03-06), carol (03-05), bob (03-03)
+286
appview/test/support/test_helpers.ex
··· 1 + defmodule Atvouch.Test.Helpers do 2 + @moduledoc false 3 + 4 + # --- DB Sandbox --- 5 + 6 + def checkout_sandbox do 7 + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 8 + Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 9 + end 10 + 11 + # --- Application Env --- 12 + 13 + def set_env(key, value), do: set_env(:atvouch, key, value) 14 + 15 + def set_env(app, key, value) do 16 + prev = Application.get_env(app, key) 17 + Application.put_env(app, key, value) 18 + prev 19 + end 20 + 21 + def restore_env(key, prev), do: restore_env(:atvouch, key, prev) 22 + 23 + def restore_env(app, key, prev) do 24 + if prev do 25 + Application.put_env(app, key, prev) 26 + else 27 + Application.delete_env(app, key) 28 + end 29 + end 30 + 31 + # --- Server Lifecycle --- 32 + 33 + def safe_stop_supervisor(pid) do 34 + try do 35 + Supervisor.stop(pid, :normal, 1_000) 36 + catch 37 + :exit, _ -> :ok 38 + end 39 + end 40 + 41 + def safe_stop_genserver(pid) do 42 + try do 43 + GenServer.stop(pid, :normal, 1_000) 44 + catch 45 + :exit, _ -> :ok 46 + end 47 + end 48 + 49 + # --- TAP Socket --- 50 + 51 + def start_tap_socket(port, opts \\ []) do 52 + handler = Keyword.get(opts, :handler, Atvouch.TapHandler) 53 + name = Keyword.get(opts, :name, :"tap_socket_#{System.unique_integer([:positive])}") 54 + 55 + {:ok, _pid} = 56 + Atvouch.Tap.Socket.start_link( 57 + uri: "ws://localhost:#{port}/channel", 58 + handler: handler, 59 + password: "123", 60 + name: name 61 + ) 62 + 63 + receive do 64 + {:ws_connected, ws_pid} -> ws_pid 65 + after 66 + 5_000 -> raise "Timed out waiting for WebSocket connection" 67 + end 68 + end 69 + 70 + # --- Auth Server Setup --- 71 + 72 + def setup_auth_server do 73 + {_server_pid, auth_port} = Atvouch.Test.FakeAuthServer.start() 74 + auth_url = "http://127.0.0.1:#{auth_port}" 75 + prev = set_env(:auth_url, auth_url) 76 + {auth_url, prev} 77 + end 78 + 79 + # --- Data Builders --- 80 + 81 + def create_identity(did, handle) do 82 + {:ok, identity} = Atvouch.Identity.create(%{did: did, handle: handle}) 83 + identity 84 + end 85 + 86 + def create_vouch(creator_did, target_did, opts \\ []) do 87 + created_at = Keyword.get(opts, :created_at, "2026-03-01T00:00:00Z") 88 + cid = Keyword.get(opts, :cid, "bafytest#{:erlang.phash2({creator_did, target_did})}") 89 + live = Keyword.get(opts, :live, true) 90 + 91 + {:ok, vouch} = 92 + Atvouch.Vouch.create(%{ 93 + at_uri: "at://#{creator_did}/dev.atvouch.graph.vouch/#{target_did}", 94 + creator_did: creator_did, 95 + target_did: target_did, 96 + original_created_at: created_at, 97 + remote_created_at: created_at, 98 + at_cid: cid, 99 + live: live 100 + }) 101 + 102 + vouch 103 + end 104 + 105 + # --- TAP Event Builders --- 106 + 107 + def vouch_record_event(id, creator_did, target_did, opts \\ []) do 108 + action = Keyword.get(opts, :action, "create") 109 + live = Keyword.get(opts, :live, false) 110 + created_at = Keyword.get(opts, :created_at, "2026-03-09T10:53:26.922Z") 111 + cid = Keyword.get(opts, :cid, "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju") 112 + rev = Keyword.get(opts, :rev, "3mgmqjki6sz2n") 113 + 114 + record_base = %{ 115 + "action" => action, 116 + "did" => creator_did, 117 + "rev" => rev, 118 + "collection" => "dev.atvouch.graph.vouch", 119 + "rkey" => target_did 120 + } 121 + 122 + record = 123 + if action == "delete" do 124 + record_base 125 + else 126 + record_base 127 + |> Map.put("record", %{ 128 + "$type" => "dev.atvouch.graph.vouch", 129 + "createdAt" => created_at, 130 + "subject" => target_did 131 + }) 132 + |> Map.put("cid", cid) 133 + |> Map.put("live", live) 134 + end 135 + 136 + %{"id" => id, "type" => "record", "record" => record} 137 + end 138 + 139 + def membership_record_event(id, source_did, repo_at_uri, maintainers, opts \\ []) do 140 + action = Keyword.get(opts, :action, "create") 141 + rkey = Keyword.get(opts, :rkey) 142 + cid = Keyword.get(opts, :cid, "bafyreig2uvo5annaomcmrjfrwyshnjannfrr2xje5txnsyuk7l4o2tdmju") 143 + rev = Keyword.get(opts, :rev, "3mgmqjki6sz2n") 144 + live = Keyword.get(opts, :live, false) 145 + 146 + # Extract rkey from repo_at_uri if not provided 147 + rkey = rkey || repo_at_uri |> String.split("/") |> List.last() 148 + 149 + record_base = %{ 150 + "action" => action, 151 + "did" => source_did, 152 + "rev" => rev, 153 + "collection" => "dev.atvouch.bot.membership", 154 + "rkey" => rkey 155 + } 156 + 157 + record = 158 + if action == "delete" do 159 + record_base 160 + else 161 + record_base 162 + |> Map.put("record", %{ 163 + "$type" => "dev.atvouch.bot.membership", 164 + "repo" => repo_at_uri, 165 + "maintainers" => maintainers 166 + }) 167 + |> Map.put("cid", cid) 168 + |> Map.put("live", live) 169 + end 170 + 171 + %{"id" => id, "type" => "record", "record" => record} 172 + end 173 + 174 + def pull_record_event(id, author_did, repo_at_uri, opts \\ []) do 175 + rkey = Keyword.get(opts, :rkey, "3pull#{id}") 176 + title = Keyword.get(opts, :title, "Test PR") 177 + created_at = Keyword.get(opts, :created_at, "2026-03-19T10:00:00Z") 178 + cid = Keyword.get(opts, :cid, "bafypull#{id}") 179 + live = Keyword.get(opts, :live, true) 180 + 181 + %{ 182 + "id" => id, 183 + "type" => "record", 184 + "record" => %{ 185 + "action" => "create", 186 + "did" => author_did, 187 + "rev" => "3mgmqjki6sz2n", 188 + "collection" => "sh.tangled.repo.pull", 189 + "rkey" => rkey, 190 + "record" => %{ 191 + "$type" => "sh.tangled.repo.pull", 192 + "title" => title, 193 + "createdAt" => created_at, 194 + "target" => %{ 195 + "repo" => repo_at_uri, 196 + "branch" => "main" 197 + } 198 + }, 199 + "cid" => cid, 200 + "live" => live 201 + } 202 + } 203 + end 204 + 205 + def identity_event(id, did, opts \\ []) do 206 + handle = Keyword.get(opts, :handle, "#{did |> String.split(":") |> List.last()}.bsky.social") 207 + is_active = Keyword.get(opts, :is_active, true) 208 + status = Keyword.get(opts, :status, "active") 209 + 210 + %{ 211 + "id" => id, 212 + "type" => "identity", 213 + "identity" => %{ 214 + "did" => did, 215 + "handle" => handle, 216 + "is_active" => is_active, 217 + "status" => status 218 + } 219 + } 220 + end 221 + 222 + # --- Drain Messages --- 223 + 224 + def drain_messages(timeout \\ 100) do 225 + receive do 226 + _ -> drain_messages(timeout) 227 + after 228 + timeout -> :ok 229 + end 230 + end 231 + 232 + # --- Tangled + PDS + Session combo --- 233 + 234 + def start_tangled_pds(test_pid, opts \\ []) do 235 + handle = Keyword.get(opts, :handle, "bot.test") 236 + password = Keyword.get(opts, :password, "test-password") 237 + 238 + {tangled_pid, tangled_port, state_agent} = 239 + Atvouch.Test.FakeTangledServer.start(test_pid) 240 + 241 + {pds_pid, pds_port} = 242 + Atvouch.Test.FakePdsServer.start(test_pid, 243 + expected_username: handle, 244 + expected_password: password, 245 + callback_url: "http://127.0.0.1:#{tangled_port}/oauth/callback?code=test&state=test" 246 + ) 247 + 248 + Atvouch.Test.FakeTangledServer.set_pds_url(state_agent, "http://127.0.0.1:#{pds_port}") 249 + 250 + %{ 251 + tangled_pid: tangled_pid, 252 + tangled_port: tangled_port, 253 + pds_pid: pds_pid, 254 + state_agent: state_agent, 255 + tangled_url: "http://127.0.0.1:#{tangled_port}" 256 + } 257 + end 258 + 259 + def start_tangled_pds_session(test_pid, opts \\ []) do 260 + handle = Keyword.get(opts, :handle, "bot.test") 261 + password = Keyword.get(opts, :password, "test-password") 262 + session_name = Keyword.get(opts, :session_name, Atvouch.Tangled.Session) 263 + 264 + servers = start_tangled_pds(test_pid, handle: handle, password: password) 265 + 266 + # Stop any existing global session 267 + if session_name == Atvouch.Tangled.Session do 268 + case GenServer.whereis(session_name) do 269 + nil -> :ok 270 + old_pid -> 271 + safe_stop_genserver(old_pid) 272 + Process.sleep(10) 273 + end 274 + end 275 + 276 + {:ok, session_pid} = 277 + Atvouch.Tangled.Session.start_link( 278 + handle: handle, 279 + password: password, 280 + tangled_url: servers.tangled_url, 281 + name: session_name 282 + ) 283 + 284 + Map.put(servers, :session_pid, session_pid) 285 + end 286 + end