From 5d04c48901fb9e2bb04466d573d83788588986ed Mon Sep 17 00:00:00 2001 From: Niels Mokkenstorm Date: Thu, 16 Jul 2026 21:56:41 +0200 Subject: [PATCH] test(server): cover variant grouping, stats, strategy resolution, and browse handler --- server/test/browse_handler_test.gleam | 117 +++++++++++++++ server/test/browse_test.gleam | 185 ++++++++++++++++++++---- server/test/catalog_stats_test.gleam | 95 ++++++++++++ server/test/catalog_strategy_test.gleam | 110 ++++++++++++++ server/test/catalog_variants_test.gleam | 108 ++++++++++++++ server/test/support.gleam | 25 ++++ 6 files changed, 612 insertions(+), 28 deletions(-) create mode 100644 server/test/browse_handler_test.gleam create mode 100644 server/test/catalog_stats_test.gleam create mode 100644 server/test/catalog_strategy_test.gleam create mode 100644 server/test/catalog_variants_test.gleam diff --git a/server/test/browse_handler_test.gleam b/server/test/browse_handler_test.gleam new file mode 100644 index 0000000..d25b72f --- /dev/null +++ b/server/test/browse_handler_test.gleam @@ -0,0 +1,117 @@ +//// End-to-end test for the browse handler's resolution-strategy param: +//// picking a different variant winner depending on `?strategy=`. Domain-level +//// grouping/strategy correctness lives in `catalog_variants_test`, +//// `catalog_stats_test`, and `catalog_strategy_test`; this only exercises the +//// HTTP wiring (query param parsing, `variant_source` plumbing, JSON output). + +import at_record_server/catalog/source.{type Source, Source, VariantRow} +import at_record_server/context.{type Context} +import at_record_server/handlers/browse.{browse} +import at_record_server/oauth/config +import at_record_server/oauth/session_store +import at_record_server/oauth/sessions +import gleam/http +import gleam/option.{None, Some} +import gleam/string +import support +import wisp +import wisp/simulate + +const session_cookie = "ar_oauth_sid" + +const far_future = 9_999_999_999 + +fn a_session() -> sessions.OauthSession { + support.stub_session_with( + access_token: "at", + refresh_token: "rt", + expires_at: far_future, + ) +} + +/// Two variants of the same Discogs release: one with far more adoption, +/// the other more recently created, so the two strategies disagree. +fn variant_source() -> Source { + let more_adopted = + VariantRow( + uri: "at://a/1", + cid: "bafya", + title: "Spiderland", + artist_display: Some("Slint"), + genres: [], + styles: [], + released: None, + country: None, + cover: None, + thumb_url: None, + discogs_id: Some("42"), + created_at: "2020-01-01T00:00:00Z", + publisher_did: "did:plc:pub", + publisher_handle: "pub.test", + publisher_pds: "https://pds.test", + supersedes: None, + based_on: None, + ) + let more_recent = + VariantRow( + ..more_adopted, + uri: "at://b/1", + cid: "bafyb", + created_at: "2024-01-01T00:00:00Z", + ) + Source( + releases: fn() { [more_adopted, more_recent] }, + adoption_count: fn(uri) { + case uri { + "at://a/1" -> 10 + "at://b/1" -> 1 + _ -> 0 + } + }, + ) +} + +fn test_context() -> #(Context, config.Config) { + let cfg = + support.stub_config_with( + support.unreachable_client(), + "https://resolver.test", + "http://localhost:8080", + ) + let ctx = support.stub_context_with_variant_source(cfg, variant_source()) + #(ctx, cfg) +} + +fn browse_body(query: 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.catalog.listReleases" <> query, + ) + |> simulate.cookie(session_cookie, id, wisp.Signed) + let resp = browse(req, ctx) + assert resp.status == 200 + simulate.read_body(resp) +} + +pub fn default_strategy_picks_the_most_adopted_variant_test() { + let #(ctx, cfg) = test_context() + let body = browse_body("", ctx, cfg) + assert string.contains(body, "\"uri\":\"at://a/1\"") + assert !string.contains(body, "\"uri\":\"at://b/1\"") + assert string.contains(body, "\"variantCount\":2") +} + +pub fn most_recent_strategy_switches_the_winner_test() { + let #(ctx, cfg) = test_context() + let body = browse_body("?strategy=most-recent", ctx, cfg) + assert string.contains(body, "\"uri\":\"at://b/1\"") + assert !string.contains(body, "\"uri\":\"at://a/1\"") +} + +pub fn an_unknown_strategy_falls_back_to_the_default_test() { + let #(ctx, cfg) = test_context() + let body = browse_body("?strategy=not-a-real-strategy", ctx, cfg) + assert string.contains(body, "\"uri\":\"at://a/1\"") +} diff --git a/server/test/browse_test.gleam b/server/test/browse_test.gleam index cfe85db..33976e6 100644 --- a/server/test/browse_test.gleam +++ b/server/test/browse_test.gleam @@ -2,13 +2,14 @@ import at_record/gen/defs.{CatalogRef, ExternalId} import at_record/gen/shelf/entry.{type ShelfEntry, ShelfEntry} import at_record/storage.{type StoredItem, StoredItem} import at_record_server/browse.{type BrowseRow, BrowseRow, NetworkMatch} +import at_record_server/catalog/source.{type VariantRow, VariantRow} +import at_record_server/catalog/strategy.{MostAdopted, MostRecent} import at_record_server/known_users.{KnownUser} import atproto/xrpc import gleam/bit_array import gleam/http/response import gleam/list import gleam/option.{type Option, None, Some} -import gleam/string import support fn row( @@ -37,6 +38,34 @@ fn row( ) } +fn variant_row( + uri uri: String, + discogs_id discogs_id: Option(String), + created_at created_at: String, + supersedes supersedes: Option(String), + based_on based_on: Option(String), +) -> VariantRow { + VariantRow( + uri:, + cid: "bafyrow", + title: "Title", + artist_display: None, + genres: [], + styles: [], + released: None, + country: None, + cover: None, + thumb_url: None, + discogs_id:, + created_at:, + publisher_did: "did:plc:pub", + publisher_handle: "pub.test", + publisher_pds: "https://pds.test", + supersedes:, + based_on:, + ) +} + fn genesis( rkey rkey: String, action action: String, @@ -66,38 +95,135 @@ fn genesis( ) } -pub fn dedup_keeps_first_per_discogs_id_test() { +pub fn resolve_variants_picks_one_winner_per_discogs_id_test() { let rows = [ - row(uri: "at://a/1", discogs_id: Some("42"), created_at: "z"), - row(uri: "at://b/2", discogs_id: Some("42"), created_at: "y"), - row(uri: "at://c/3", discogs_id: Some("99"), created_at: "x"), + variant_row( + uri: "at://a/1", + discogs_id: Some("42"), + created_at: "z", + supersedes: None, + based_on: None, + ), + variant_row( + uri: "at://b/2", + discogs_id: Some("42"), + created_at: "y", + supersedes: None, + based_on: None, + ), + variant_row( + uri: "at://c/3", + discogs_id: Some("99"), + created_at: "x", + supersedes: None, + based_on: None, + ), ] - let kept = browse.dedup(rows) - assert list.map(kept, fn(r) { r.uri }) == ["at://a/1", "at://c/3"] + let resolved = browse.resolve_variants(rows, fn(_uri) { 0 }, MostAdopted) + assert list.length(resolved) == 2 } -pub fn dedup_keeps_all_rows_without_discogs_id_test() { +pub fn resolve_variants_keeps_every_row_without_a_natural_key_or_chain_edge_test() { let rows = [ - row(uri: "at://a/1", discogs_id: None, created_at: "z"), - row(uri: "at://b/2", discogs_id: None, created_at: "y"), + variant_row( + uri: "at://a/1", + discogs_id: None, + created_at: "z", + supersedes: None, + based_on: None, + ), + variant_row( + uri: "at://b/2", + discogs_id: None, + created_at: "y", + supersedes: None, + based_on: None, + ), ] - let kept = browse.dedup(rows) - assert list.length(kept) == 2 + let resolved = browse.resolve_variants(rows, fn(_uri) { 0 }, MostAdopted) + assert list.length(resolved) == 2 } -pub fn dedup_after_sort_keeps_newest_created_at_regardless_of_input_order_test() { +pub fn resolve_variants_picks_most_adopted_by_default_test() { + let less_adopted = + variant_row( + uri: "at://old/1", + discogs_id: Some("42"), + created_at: "2020-01-01", + supersedes: None, + based_on: None, + ) + let more_adopted = + variant_row( + uri: "at://new/1", + discogs_id: Some("42"), + created_at: "2024-01-01", + supersedes: None, + based_on: None, + ) + let adoption_count = fn(uri) { + case uri { + "at://old/1" -> 5 + "at://new/1" -> 1 + _ -> 0 + } + } + let assert [resolved] = + browse.resolve_variants( + [less_adopted, more_adopted], + adoption_count, + MostAdopted, + ) + assert resolved.row.uri == "at://old/1" + assert resolved.stats.adoption == 5 + assert resolved.variant_count == 2 +} + +pub fn resolve_variants_switches_winner_under_most_recent_test() { let older = - row(uri: "at://old/1", discogs_id: Some("42"), created_at: "2020-01-01") + variant_row( + uri: "at://old/1", + discogs_id: Some("42"), + created_at: "2020-01-01", + supersedes: None, + based_on: None, + ) let newer = - row(uri: "at://new/1", discogs_id: Some("42"), created_at: "2024-01-01") - let sort_desc_and_dedup = fn(rows: List(BrowseRow)) { - rows - |> list.sort(fn(a, b) { string.compare(b.created_at, a.created_at) }) - |> browse.dedup - |> list.map(fn(r) { r.uri }) - } - assert sort_desc_and_dedup([older, newer]) == ["at://new/1"] - assert sort_desc_and_dedup([newer, older]) == ["at://new/1"] + variant_row( + uri: "at://new/1", + discogs_id: Some("42"), + created_at: "2024-01-01", + supersedes: None, + based_on: None, + ) + let assert [resolved] = + browse.resolve_variants([older, newer], fn(_uri) { 0 }, MostRecent) + assert resolved.row.uri == "at://new/1" +} + +pub fn resolve_variants_groups_a_basedon_fork_without_a_shared_discogs_id_test() { + let original = + variant_row( + uri: "at://a/1", + discogs_id: Some("42"), + created_at: "2020-01-01", + supersedes: None, + based_on: None, + ) + let fork = + variant_row( + uri: "at://b/1", + discogs_id: Some("99"), + created_at: "2024-01-01", + supersedes: None, + based_on: Some("at://a/1"), + ) + let resolved = + browse.resolve_variants([original, fork], fn(_uri) { 0 }, MostRecent) + assert list.length(resolved) == 1 + let assert [winner] = resolved + assert winner.variant_count == 2 + assert winner.row.uri == "at://b/1" } pub fn year_from_released_parses_leading_four_digits_test() { @@ -130,8 +256,8 @@ pub fn ownership_matches_by_release_uri_test() { row(uri: "at://did:plc:pub/rel/1", discogs_id: None, created_at: "z") let miss = row(uri: "at://did:plc:pub/rel/2", discogs_id: None, created_at: "z") - assert browse.flags(own, hit) == #(True, False) - assert browse.flags(own, miss) == #(False, False) + assert browse.flags(own, hit.uri, hit.discogs_id) == #(True, False) + assert browse.flags(own, miss.uri, miss.discogs_id) == #(False, False) } pub fn ownership_matches_by_discogs_id_across_status_test() { @@ -150,9 +276,12 @@ pub fn ownership_matches_by_discogs_id_across_status_test() { row(uri: "at://other/y", discogs_id: Some("99"), created_at: "z") let unknown_row = row(uri: "at://other/z", discogs_id: Some("7"), created_at: "z") - assert browse.flags(own, owned_row) == #(True, False) - assert browse.flags(own, wanted_row) == #(False, True) - assert browse.flags(own, unknown_row) == #(False, False) + assert browse.flags(own, owned_row.uri, owned_row.discogs_id) + == #(True, False) + assert browse.flags(own, wanted_row.uri, wanted_row.discogs_id) + == #(False, True) + assert browse.flags(own, unknown_row.uri, unknown_row.discogs_id) + == #(False, False) } // --- find_by_barcode: network-first fan-out --- diff --git a/server/test/catalog_stats_test.gleam b/server/test/catalog_stats_test.gleam new file mode 100644 index 0000000..f498f00 --- /dev/null +++ b/server/test/catalog_stats_test.gleam @@ -0,0 +1,95 @@ +import at_record_server/catalog/source.{type VariantRow, VariantRow} +import at_record_server/catalog/stats +import atproto/blob.{Blob} +import gleam/dict +import gleam/option.{None, Some} + +fn bare_row(uri: String) -> VariantRow { + VariantRow( + uri:, + cid: "bafyrow", + title: "Title", + artist_display: None, + genres: [], + styles: [], + released: None, + country: None, + cover: None, + thumb_url: None, + discogs_id: None, + created_at: "2026-01-01T00:00:00Z", + publisher_did: "did:plc:pub", + publisher_handle: "pub.test", + publisher_pds: "https://pds.test", + supersedes: None, + based_on: None, + ) +} + +pub fn completeness_is_zero_when_every_optional_field_is_absent_test() { + assert stats.completeness(bare_row("at://a/1")) == 0.0 +} + +pub fn completeness_is_one_when_every_optional_field_is_present_test() { + let full = + VariantRow( + ..bare_row("at://a/1"), + artist_display: Some("Slint"), + released: Some("1991"), + country: Some("US"), + cover: Some(Blob(cid: "bafycover", mime_type: "image/jpeg", size: 100)), + thumb_url: Some("https://example.test/thumb.jpg"), + discogs_id: Some("42"), + ) + assert stats.completeness(full) == 1.0 +} + +pub fn completeness_is_a_fraction_for_partial_rows_test() { + let half = + VariantRow( + ..bare_row("at://a/1"), + artist_display: Some("Slint"), + country: Some("US"), + released: Some("1991"), + ) + assert stats.completeness(half) == 3.0 /. 6.0 +} + +pub fn completeness_ignores_chain_edges_test() { + let with_chain = + VariantRow( + ..bare_row("at://a/1"), + supersedes: Some("at://old/1"), + based_on: Some("at://origin/1"), + ) + assert stats.completeness(with_chain) == 0.0 +} + +pub fn compute_reads_adoption_off_the_injected_counter_test() { + let row = bare_row("at://a/1") + let counted = + stats.compute(row, fn(uri) { + case uri { + "at://a/1" -> 7 + _ -> 0 + } + }) + assert counted.adoption == 7 + assert counted.created_at == row.created_at +} + +pub fn compute_all_keys_stats_by_uri_test() { + let rows = [bare_row("at://a/1"), bare_row("at://b/1")] + let all = + stats.compute_all(rows, fn(uri) { + case uri { + "at://a/1" -> 3 + "at://b/1" -> 9 + _ -> 0 + } + }) + let assert Ok(a) = dict.get(all, "at://a/1") + let assert Ok(b) = dict.get(all, "at://b/1") + assert a.adoption == 3 + assert b.adoption == 9 +} diff --git a/server/test/catalog_strategy_test.gleam b/server/test/catalog_strategy_test.gleam new file mode 100644 index 0000000..5e1de4d --- /dev/null +++ b/server/test/catalog_strategy_test.gleam @@ -0,0 +1,110 @@ +import at_record_server/catalog/source.{type VariantRow, VariantRow} +import at_record_server/catalog/stats.{VariantStats} +import at_record_server/catalog/strategy +import gleam/dict +import gleam/list +import gleam/option.{None, Some} + +fn variant_row(uri: String, created_at: String) -> VariantRow { + VariantRow( + uri:, + cid: "bafyrow", + title: "Title", + artist_display: None, + genres: [], + styles: [], + released: None, + country: None, + cover: None, + thumb_url: None, + discogs_id: None, + created_at:, + publisher_did: "did:plc:pub", + publisher_handle: "pub.test", + publisher_pds: "https://pds.test", + supersedes: None, + based_on: None, + ) +} + +fn stats_of( + entries: List(#(VariantRow, Int, String, Float)), +) -> #(List(VariantRow), dict.Dict(String, stats.VariantStats)) { + let rows = entries |> list.map(fn(e) { e.0 }) + let table = + entries + |> list.map(fn(e) { + let #(row, adoption, created_at, completeness) = e + #(row.uri, VariantStats(adoption:, created_at:, completeness:)) + }) + |> dict.from_list + #(rows, table) +} + +pub fn parse_accepts_the_three_known_strategies_test() { + assert strategy.parse("most-adopted") == Ok(strategy.MostAdopted) + assert strategy.parse("most-recent") == Ok(strategy.MostRecent) + assert strategy.parse("most-complete") == Ok(strategy.MostComplete) +} + +pub fn parse_rejects_anything_else_test() { + assert strategy.parse("") == Error(Nil) + assert strategy.parse("MostAdopted") == Error(Nil) + assert strategy.parse("random") == Error(Nil) +} + +pub fn default_strategy_is_most_adopted_test() { + assert strategy.default_strategy() == strategy.MostAdopted +} + +pub fn resolve_most_adopted_picks_the_higher_adoption_count_test() { + let a = variant_row("at://a/1", "2020-01-01") + let b = variant_row("at://b/1", "2020-01-01") + let #(rows, table) = + stats_of([#(a, 3, "2020-01-01", 0.5), #(b, 9, "2020-01-01", 0.5)]) + assert strategy.resolve(rows, table, strategy.MostAdopted) == Some(b) +} + +pub fn resolve_most_recent_picks_the_newer_created_at_test() { + let a = variant_row("at://a/1", "2020-01-01") + let b = variant_row("at://b/1", "2024-01-01") + let #(rows, table) = + stats_of([#(a, 0, "2020-01-01", 0.5), #(b, 0, "2024-01-01", 0.5)]) + assert strategy.resolve(rows, table, strategy.MostRecent) == Some(b) +} + +pub fn resolve_most_complete_picks_the_higher_completeness_test() { + let a = variant_row("at://a/1", "2020-01-01") + let b = variant_row("at://b/1", "2020-01-01") + let #(rows, table) = + stats_of([#(a, 0, "2020-01-01", 0.2), #(b, 0, "2020-01-01", 0.9)]) + assert strategy.resolve(rows, table, strategy.MostComplete) == Some(b) +} + +pub fn resolve_breaks_a_full_tie_on_created_at_then_uri_test() { + let a = variant_row("at://z/1", "2020-01-01") + let b = variant_row("at://a/1", "2020-01-01") + let #(rows, table) = + stats_of([#(a, 5, "2020-01-01", 0.5), #(b, 5, "2020-01-01", 0.5)]) + // Equal adoption -> falls through to created_at (also equal) -> uri asc: + // "at://a/1" sorts before "at://z/1". + assert strategy.resolve(rows, table, strategy.MostAdopted) == Some(b) +} + +pub fn resolve_breaks_a_metric_tie_on_created_at_before_uri_test() { + let older_but_alphabetically_later = variant_row("at://z/1", "2019-01-01") + let newer_but_alphabetically_earlier = variant_row("at://a/1", "2021-01-01") + let #(rows, table) = + stats_of([ + #(older_but_alphabetically_later, 5, "2019-01-01", 0.5), + #(newer_but_alphabetically_earlier, 5, "2021-01-01", 0.5), + ]) + // Equal adoption -> tie-break is created_at ascending, which picks the + // 2019 row even though "at://z/1" sorts after "at://a/1". + assert strategy.resolve(rows, table, strategy.MostAdopted) + == Some(older_but_alphabetically_later) +} + +pub fn resolve_of_an_empty_set_is_none_test() { + assert strategy.resolve([], dict.new(), strategy.MostAdopted) == None +} diff --git a/server/test/catalog_variants_test.gleam b/server/test/catalog_variants_test.gleam new file mode 100644 index 0000000..a44ee1e --- /dev/null +++ b/server/test/catalog_variants_test.gleam @@ -0,0 +1,108 @@ +import at_record_server/catalog/source.{type VariantRow, VariantRow} +import at_record_server/catalog/variants +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/set + +fn variant_row( + uri uri: String, + discogs_id discogs_id: Option(String), + supersedes supersedes: Option(String), + based_on based_on: Option(String), +) -> VariantRow { + VariantRow( + uri:, + cid: "bafyrow", + title: "Title", + artist_display: None, + genres: [], + styles: [], + released: None, + country: None, + cover: None, + thumb_url: None, + discogs_id:, + created_at: "2026-01-01T00:00:00Z", + publisher_did: "did:plc:pub", + publisher_handle: "pub.test", + publisher_pds: "https://pds.test", + supersedes:, + based_on:, + ) +} + +fn flat_uris(sets: List(variants.VariantSet)) -> List(String) { + sets |> list.flatten |> list.map(fn(row) { row.uri }) +} + +pub fn group_by_shared_discogs_id_test() { + let a = variant_row("at://a/1", Some("42"), None, None) + let b = variant_row("at://b/1", Some("42"), None, None) + let c = variant_row("at://c/1", Some("99"), None, None) + let sets = variants.group([a, b, c]) + assert list.length(sets) == 2 + assert list.any(sets, fn(s) { list.length(s) == 2 }) + assert list.any(sets, fn(s) { list.length(s) == 1 }) +} + +pub fn group_keeps_rows_without_a_natural_key_or_chain_edge_separate_test() { + let a = variant_row("at://a/1", None, None, None) + let b = variant_row("at://b/1", None, None, None) + let sets = variants.group([a, b]) + assert list.length(sets) == 2 +} + +pub fn group_merges_a_based_on_fork_with_no_shared_discogs_id_test() { + let original = variant_row("at://a/1", Some("42"), None, None) + let fork = variant_row("at://b/1", Some("99"), None, Some("at://a/1")) + let sets = variants.group([original, fork]) + assert list.length(sets) == 1 + let assert [only] = sets + assert list.length(only) == 2 +} + +pub fn group_merges_a_supersedes_chain_test() { + let original = variant_row("at://a/1", Some("42"), None, None) + let replacement = variant_row("at://b/1", Some("99"), Some("at://a/1"), None) + let sets = variants.group([original, replacement]) + assert list.length(sets) == 1 +} + +pub fn group_transitively_merges_a_multi_hop_chain_test() { + // a <- supersedes -- b <- supersedes -- c, each with its own discogs id. + let a = variant_row("at://a/1", Some("1"), None, None) + let b = variant_row("at://b/1", Some("2"), Some("at://a/1"), None) + let c = variant_row("at://c/1", Some("3"), Some("at://b/1"), None) + let sets = variants.group([a, b, c]) + assert list.length(sets) == 1 + let assert [only] = sets + assert list.length(only) == 3 +} + +pub fn group_survives_a_mutual_supersedes_cycle_test() { + // A cyclic pair (data bug or adversarial input): each supersedes the + // other. The visited-set walk must still terminate and yield one group, + // not loop forever or duplicate a row. + let a = variant_row("at://a/1", Some("1"), Some("at://b/1"), None) + let b = variant_row("at://b/1", Some("2"), Some("at://a/1"), None) + let sets = variants.group([a, b]) + assert list.length(sets) == 1 + let assert [only] = sets + assert list.length(only) == 2 +} + +pub fn group_every_input_row_appears_in_exactly_one_output_set_test() { + let a = variant_row("at://a/1", Some("42"), None, None) + let b = variant_row("at://b/1", Some("42"), None, None) + let c = variant_row("at://c/1", None, None, None) + let sets = variants.group([a, b, c]) + let flat = flat_uris(sets) + assert list.length(flat) == 3 + assert set.from_list(flat) + == set.from_list(["at://a/1", "at://b/1", "at://c/1"]) +} + +pub fn group_uses_uri_as_a_singleton_fallback_when_discogs_id_is_absent_test() { + let lone = variant_row("at://lone/1", None, None, None) + assert variants.group([lone]) == [[lone]] +} diff --git a/server/test/support.gleam b/server/test/support.gleam index d5b544b..30c0ed1 100644 --- a/server/test/support.gleam +++ b/server/test/support.gleam @@ -6,6 +6,7 @@ import at_record/gen/catalog/release as catalog_release import at_record/gen/shelf/entry +import at_record_server/catalog/source as catalog_source import at_record_server/catalog_deps import at_record_server/catalog_index import at_record_server/context.{type Context, Atproto, Context, Discogs, Web} @@ -59,6 +60,12 @@ fn empty_catalog_index() -> catalog_index.Store { ) } +/// A `variant_source` with no candidate rows and a zero adoption count for +/// every uri; use `stub_context_with_variant_source` to override it. +pub fn empty_variant_source() -> catalog_source.Source { + catalog_source.Source(releases: fn() { [] }, adoption_count: fn(_) { 0 }) +} + /// A `Config` wired to in-memory pending-flow/session stores, so tests never /// touch a real database. `resolver`/`base_url` default to a plain localhost /// dev client; use `stub_config_with` to exercise the confidential-client @@ -110,10 +117,28 @@ pub fn stub_context_with( catalog:, known_users: known_users_of(known_users), catalog_index: empty_catalog_index(), + variant_source: empty_variant_source(), oauth: cfg, ) } +/// `stub_context_with`, but also overriding `variant_source`: what the +/// browse-handler resolution-strategy tests actually vary. +pub fn stub_context_with_variant_source( + cfg: config.Config, + variant_source: catalog_source.Source, +) -> Context { + Context( + ..stub_context_with( + cfg, + unreachable_catalog_deps(), + fn(_req) { Error("unused") }, + [], + ), + variant_source:, + ) +} + /// A minimal `CatalogRelease`: only `title` and `created_at` are set, every /// other field `None`. Override whichever fields a test cares about via /// `CatalogRelease(..blank_catalog_release(), field: value)`. -- 2.51.2