//// End-to-end scan handler tests for the "did you mean" recovery flow: the //// exact barcode misses on Discogs, variants miss too, then MusicBrainz seeds //// a Discogs text search whose hits become ranked suggestions. The Discogs //// sender is stubbed once and branches on whether the request carries a //// `barcode` param (lookup) or a `q` param (text search); the MusicBrainz //// call goes through the atproto xrpc client, stubbed independently. import at_record_server/context.{type Context} import at_record_server/discogs_client import at_record_server/handlers/discogs as discogs_handler import at_record_server/known_users.{type KnownUser, KnownUser} import at_record_server/oauth/config import at_record_server/oauth/session_store import at_record_server/oauth/sessions import atproto/xrpc import gleam/bit_array import gleam/http import gleam/http/request import gleam/http/response import gleam/list import gleam/result import gleam/string import support import wisp import wisp/simulate const session_cookie = "ar_oauth_sid" const far_future = 9_999_999_999 const empty_search_body = "{\"pagination\":{\"page\":1,\"pages\":1},\"results\":[]}" const search_hit_body = "{\"pagination\":{\"page\":1,\"pages\":1},\"results\":[{\"id\":123,\"title\":\"Muse - Black Holes And Revelations\",\"year\":\"2006\",\"format\":[\"CD\",\"Album\"],\"thumb\":\"\",\"cover_image\":\"\"},{\"id\":456,\"title\":\"Nobody - Totally Unrelated Thing\",\"year\":\"2001\",\"format\":[\"CD\"],\"thumb\":\"\",\"cover_image\":\"\"}]}" const mb_barcode_body = "{\"count\":1,\"offset\":0,\"releases\":[{\"id\":\"9ec684cf-7529-300f-b6c4-6c56086dfe93\",\"score\":100,\"title\":\"Black Holes and Revelations\",\"artist-credit\":[{\"name\":\"Muse\"}]}]}" /// A Discogs sender that never finds a barcode but does find the text search /// seeded from the MusicBrainz hint. fn discogs_recovers_via_text_search( req: request.Request(String), ) -> Result(#(Int, String), String) { case has_barcode_param(req) { True -> Ok(#(200, empty_search_body)) False -> Ok(#(200, search_hit_body)) } } fn discogs_always_misses( _req: request.Request(String), ) -> Result(#(Int, String), String) { Ok(#(200, empty_search_body)) } fn has_barcode_param(req: request.Request(String)) -> Bool { request.get_query(req) |> result.unwrap([]) |> list.key_find("barcode") |> result.is_ok } fn mb_returns_exact_match() -> xrpc.Client { xrpc.Client(send: fn(_req) { Ok(response.Response(200, [], bit_array.from_string(mb_barcode_body))) }) } fn mb_fails() -> xrpc.Client { xrpc.Client(send: fn(_req) { Error("musicbrainz unreachable") }) } fn a_session() -> sessions.OauthSession { support.stub_session_with( access_token: "at", refresh_token: "rt", expires_at: far_future, ) } fn test_context( mb: xrpc.Client, discogs_send: discogs_client.Sender, ) -> #(Context, config.Config) { let cfg = support.stub_config_with(mb, "r", "http://localhost:8080") let ctx = support.stub_context_with( cfg, support.unreachable_catalog_deps(), discogs_send, [], ) #(ctx, cfg) } fn authenticated_scan( barcode: String, ctx: Context, cfg: config.Config, ) -> String { let assert Ok(id) = session_store.create(cfg.sessions, a_session()) let req = simulate.request( http.Get, "/xrpc/dev.mokkenstorm.crate.discogs.resolveBarcode?barcode=" <> barcode, ) |> simulate.cookie(session_cookie, id, wisp.Signed) let resp = discogs_handler.scan(req, ctx) assert resp.status == 200 simulate.read_body(resp) } pub fn scan_miss_bridges_to_musicbrainz_suggestions_test() { let #(ctx, cfg) = test_context(mb_returns_exact_match(), discogs_recovers_via_text_search) let body = authenticated_scan("825646350919", ctx, cfg) assert string.contains(body, "\"result\":null") assert string.contains(body, "\"suggestions\"") // The exact-title hit scores a full 100 against the MusicBrainz hint. assert string.contains(body, "\"confidence\":100") } pub fn scan_musicbrainz_failure_degrades_to_plain_no_match_test() { let #(ctx, cfg) = test_context(mb_fails(), discogs_always_misses) let body = authenticated_scan("825646350919", ctx, cfg) assert string.contains(body, "\"result\":null") assert !string.contains(body, "suggestions") } const network_hit_barcode = "01234567 89011" const network_hit_barcode_despaced = "0123456789011" fn network_hit_records_body() -> String { "{\"records\":[{\"uri\":\"at://did:plc:pub/dev.mokkenstorm.crate.catalog.release/r1\",\"cid\":\"bafyrel\",\"value\":{\"title\":\"Spiderland\",\"artistDisplay\":\"Slint\",\"createdAt\":\"2026-01-01T00:00:00Z\",\"identifiers\":[{\"type\":\"Barcode\",\"value\":\"" <> network_hit_barcode <> "\"}]}}]}" } fn empty_records_body() -> String { "{\"records\":[]}" } fn a_known_user() -> KnownUser { KnownUser(did: "did:plc:pub", handle: "pub.test", pds: "https://pds.test") } /// A client whose `send` answers a `listRecords` fan-out call with `body`; /// used as `ctx.atproto.client` so it also stands in for the MusicBrainz /// client (unreachable in these tests: a network hit never falls through, /// and a network miss here always hits Discogs directly). fn client_with_records(body: String) -> xrpc.Client { xrpc.Client(send: fn(_req) { Ok(response.Response(200, [], bit_array.from_string(body))) }) } fn discogs_panics_if_called( _req: request.Request(String), ) -> Result(#(Int, String), String) { panic as "discogs must not be called on a network hit" } fn test_context_with_users( atproto_client: xrpc.Client, discogs_send: discogs_client.Sender, users: List(KnownUser), ) -> #(Context, config.Config) { let cfg = support.stub_config_with(atproto_client, "r", "http://localhost:8080") let ctx = support.stub_context_with( cfg, support.unreachable_catalog_deps(), discogs_send, users, ) #(ctx, cfg) } pub fn network_hit_returns_network_and_never_calls_discogs_test() { let #(ctx, cfg) = test_context_with_users( client_with_records(network_hit_records_body()), discogs_panics_if_called, [a_known_user()], ) let body = authenticated_scan(network_hit_barcode_despaced, ctx, cfg) assert string.contains(body, "\"network\"") assert string.contains( body, "\"uri\":\"at://did:plc:pub/dev.mokkenstorm.crate.catalog.release/r1\"", ) assert string.contains(body, "\"title\":\"Spiderland\"") assert string.contains(body, "\"publisherHandle\":\"pub.test\"") } fn discogs_hits_immediately( _req: request.Request(String), ) -> Result(#(Int, String), String) { Ok(#(200, search_hit_body)) } pub fn network_miss_falls_through_to_the_discogs_chain_test() { let #(ctx, cfg) = test_context_with_users( client_with_records(empty_records_body()), discogs_hits_immediately, [a_known_user()], ) let body = authenticated_scan("825646350919", ctx, cfg) assert !string.contains(body, "\"network\"") assert string.contains(body, "Black Holes And Revelations") } pub fn network_stage_failure_falls_through_to_discogs_test() { let failing_client = xrpc.Client(send: fn(_req) { Error("unreachable") }) let #(ctx, cfg) = test_context_with_users(failing_client, discogs_always_misses, [ a_known_user(), ]) let body = authenticated_scan("825646350919", ctx, cfg) assert !string.contains(body, "\"network\"") assert string.contains(body, "\"result\":null") } pub fn network_stage_caps_the_per_user_fetch_like_browse_test() { let checked_client = xrpc.Client(send: fn(req) { case req.host { "pds.test" -> { let query = request.get_query(req) |> result.unwrap([]) assert list.key_find(query, "limit") == Ok("50") assert list.key_find(query, "collection") == Ok("dev.mokkenstorm.crate.catalog.release") Ok(response.Response( 200, [], bit_array.from_string(empty_records_body()), )) } // The Discogs miss falls through to a MusicBrainz hint lookup on the // same shared client; irrelevant to this cap assertion. _ -> Ok(response.Response( 200, [], bit_array.from_string("{\"releases\":[]}"), )) } }) let #(ctx, cfg) = test_context_with_users(checked_client, discogs_always_misses, [ a_known_user(), ]) let body = authenticated_scan("825646350919", ctx, cfg) assert !string.contains(body, "\"network\"") }