From c0eeb9e02dc6f3ffc6202dc5cef5343db5d73d9e Mon Sep 17 00:00:00 2001 From: Mateu Hunter Date: Sun, 5 Apr 2026 10:57:48 -0600 Subject: [PATCH 1/2] refactor: derive region validation scope from bracket graph --- lib/Bracket/Service/BracketValidator.pm | 38 ++++++++++++++++++++++--- t/bracket_validator.t | 27 ++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/lib/Bracket/Service/BracketValidator.pm b/lib/Bracket/Service/BracketValidator.pm index f3cdb20..443bea8 100644 --- a/lib/Bracket/Service/BracketValidator.pm +++ b/lib/Bracket/Service/BracketValidator.pm @@ -14,8 +14,7 @@ sub validate_region_payload { my ($pick_map, $parse_errors) = _extract_pick_map($params); return { ok => 0, errors => $parse_errors } if @{$parse_errors}; - my $min_game = 1 + 15 * ($region_id - 1); - my $max_game = 15 + 15 * ($region_id - 1); + my $region_game_ids = _region_game_ids_for_region($schema, $region_id); my @errors; my %existing = map { $_->game->id => $_->pick->id } @@ -24,7 +23,7 @@ sub validate_region_payload { my @changed_games; foreach my $game_id (sort { $a <=> $b } keys %{$pick_map}) { - if ($game_id < $min_game || $game_id > $max_game) { + if (!$region_game_ids->{$game_id}) { push @errors, "Game ${game_id} is outside region ${region_id}"; next; } @@ -36,7 +35,7 @@ sub validate_region_payload { \@changed_games, sub { my ($game_id) = @_; - return $game_id >= $min_game && $game_id <= $max_game; + return $region_game_ids->{$game_id}; } ); @@ -129,6 +128,37 @@ sub _extract_pick_map { return (\%pick_map, \@errors); } +sub _region_game_ids_for_region { + my ($schema, $region_id) = @_; + + my $region_winner_games = Bracket::Service::BracketStructure->region_winner_games_by_region($schema); + my $region_winner_game_id = $region_winner_games->{$region_id}; + return _fallback_region_game_ids($region_id) if !$region_winner_game_id; + + my %parents_by_game; + foreach my $edge ($schema->resultset('GameGraph')->search({})->all) { + push @{$parents_by_game{$edge->game}}, $edge->parent_game; + } + + my %allowed_games; + my @queue = ($region_winner_game_id); + while (@queue) { + my $game_id = shift @queue; + next if $allowed_games{$game_id}++; + push @queue, @{$parents_by_game{$game_id} || []}; + } + + return \%allowed_games; +} + +sub _fallback_region_game_ids { + my ($region_id) = @_; + + my $min_game = 1 + 15 * ($region_id - 1); + my $max_game = 15 + 15 * ($region_id - 1); + return { map { $_ => 1 } ($min_game .. $max_game) }; +} + sub _affected_games_in_scope { my ($schema, $changed_games, $is_in_scope) = @_; diff --git a/t/bracket_validator.t b/t/bracket_validator.t index ae98e6f..4858b23 100644 --- a/t/bracket_validator.t +++ b/t/bracket_validator.t @@ -177,5 +177,32 @@ my $bad_param = Bracket::Service::BracketValidator->validate_region_payload( ); ok(!$bad_param->{ok}, 'non-numeric team id is rejected'); +{ + no warnings 'redefine'; + local *Bracket::Service::BracketStructure::region_winner_games_by_region = sub { + return { 1 => 30 }; + }; + + my $derived_region_games = Bracket::Service::BracketValidator::_region_game_ids_for_region($schema, 1); + ok($derived_region_games->{30}, 'region game scope includes remapped region-winner game'); + ok($derived_region_games->{16}, 'region game scope follows remapped ancestry'); + ok(!$derived_region_games->{1}, 'region game scope excludes original range when topology remaps'); + + my $remapped_region_validation = Bracket::Service::BracketValidator->validate_region_payload( + $schema, + $player_id, + 1, + { + p30 => 1, + } + ); + ok(!$remapped_region_validation->{ok}, 'remapped validation still enforces pick correctness'); + unlike( + join(' ', @{$remapped_region_validation->{errors}}), + qr/outside region 1/i, + 'remapped topology is used instead of fixed 1..15 range' + ); +} + done_testing(); From a6618544c9697c5696449528afd7793902438099 Mon Sep 17 00:00:00 2001 From: Mateu X Hunter Date: Sun, 5 Apr 2026 12:28:19 -0600 Subject: [PATCH 2/2] Update lib/Bracket/Service/BracketValidator.pm Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/Bracket/Service/BracketValidator.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Bracket/Service/BracketValidator.pm b/lib/Bracket/Service/BracketValidator.pm index 443bea8..1fa7e9e 100644 --- a/lib/Bracket/Service/BracketValidator.pm +++ b/lib/Bracket/Service/BracketValidator.pm @@ -137,7 +137,11 @@ sub _region_game_ids_for_region { my %parents_by_game; foreach my $edge ($schema->resultset('GameGraph')->search({})->all) { - push @{$parents_by_game{$edge->game}}, $edge->parent_game; + my $game_id = $edge->game; + my $parent_game_id = $edge->parent_game; + next if !defined $game_id || !defined $parent_game_id; + + push @{$parents_by_game{$game_id}}, $parent_game_id; } my %allowed_games;