feat: add match endpoint filters
This commit is contained in:
parent
4bd1531408
commit
0ccce16849
|
|
@ -2,10 +2,28 @@
|
||||||
|
|
||||||
class TournamentMatchesChannel < ApplicationCable::Channel
|
class TournamentMatchesChannel < ApplicationCable::Channel
|
||||||
def subscribed
|
def subscribed
|
||||||
tournament = Tournament.find(params[:tournament_id])
|
@tournament_id = params[:tournament_id]
|
||||||
state = params[:state].presence
|
@filters = match_filters
|
||||||
|
tournament = Tournament.find(@tournament_id)
|
||||||
|
|
||||||
stream_for TournamentMatchesBroadcast.stream_target(tournament, state:)
|
stream_for TournamentMatchesBroadcast.stream_target(tournament) do
|
||||||
transmit TournamentMatchesBroadcastPayload.build(tournament, state:)
|
transmit_current_payload
|
||||||
|
end
|
||||||
|
transmit TournamentMatchesBroadcastPayload.build(tournament, @filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :tournament_id, :filters
|
||||||
|
|
||||||
|
def match_filters
|
||||||
|
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position)
|
||||||
|
end
|
||||||
|
|
||||||
|
def transmit_current_payload
|
||||||
|
tournament = TournamentMatchesBroadcast.load_tournament(tournament_id)
|
||||||
|
return if tournament.nil?
|
||||||
|
|
||||||
|
transmit TournamentMatchesBroadcastPayload.build(tournament, filters)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class MatchesController < ApplicationController
|
||||||
|
|
||||||
# GET/tournaments/1/matches
|
# GET/tournaments/1/matches
|
||||||
def index
|
def index
|
||||||
render json: TournamentMatchesPayload.build(@tournament, state: match_params['state'])
|
render json: TournamentMatchesPayload.build(@tournament, match_index_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /matches/1
|
# GET /matches/1
|
||||||
|
|
@ -149,6 +149,10 @@ class MatchesController < ApplicationController
|
||||||
params.slice(:state).permit!
|
params.slice(:state).permit!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_index_params
|
||||||
|
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position).permit!
|
||||||
|
end
|
||||||
|
|
||||||
def create_match_params
|
def create_match_params
|
||||||
params.slice(:team_ids).permit(team_ids: [])
|
params.slice(:team_ids).permit(team_ids: [])
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,21 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TournamentMatchesBroadcast
|
class TournamentMatchesBroadcast
|
||||||
STATES = [nil, 'upcoming', *Match.states.keys].freeze
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def call(tournament_id)
|
def call(tournament_id)
|
||||||
tournament = load_tournament(tournament_id)
|
tournament = load_tournament(tournament_id)
|
||||||
return if tournament.nil?
|
return if tournament.nil?
|
||||||
|
|
||||||
STATES.each do |state|
|
TournamentMatchesChannel.broadcast_to(
|
||||||
TournamentMatchesChannel.broadcast_to(
|
stream_target(tournament),
|
||||||
stream_target(tournament, state:),
|
{ type: 'tournament_matches.changed', tournament_id: tournament.id }
|
||||||
TournamentMatchesBroadcastPayload.build(tournament, state:)
|
)
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def stream_target(tournament, state:)
|
def stream_target(tournament)
|
||||||
"#{tournament.to_gid_param}:matches:#{state.presence || 'all'}"
|
"#{tournament.to_gid_param}:matches"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def load_tournament(tournament_id)
|
def load_tournament(tournament_id)
|
||||||
Tournament.includes(
|
Tournament.includes(
|
||||||
stages: [
|
stages: [
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
class TournamentMatchesBroadcastPayload
|
class TournamentMatchesBroadcastPayload
|
||||||
class << self
|
class << self
|
||||||
def build(tournament, state: nil)
|
def build(tournament, filters = {}, state: nil)
|
||||||
|
filters = TournamentMatchesPayload.normalize_filters(filters, state:)
|
||||||
{
|
{
|
||||||
type: 'tournament_matches.updated',
|
type: 'tournament_matches.updated',
|
||||||
tournament_id: tournament.id,
|
tournament_id: tournament.id,
|
||||||
state:,
|
state: filters[:state],
|
||||||
matches: TournamentMatchesPayload.build(tournament, state:)
|
matches: TournamentMatchesPayload.build(tournament, filters)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,77 @@
|
||||||
|
|
||||||
class TournamentMatchesPayload
|
class TournamentMatchesPayload
|
||||||
class << self
|
class << self
|
||||||
def build(tournament, state: nil)
|
def build(tournament, filters = {}, state: nil, **filter_keywords)
|
||||||
serialized_matches(matches_for(tournament, state:))
|
serialized_matches(matches_for(tournament, filters.merge(filter_keywords), state:))
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches_for(tournament, state: nil)
|
def matches_for(tournament, filters = {}, state: nil, **filter_keywords)
|
||||||
|
filters = normalize_filters(filters.merge(filter_keywords), state:)
|
||||||
|
matches = state_filtered_matches(tournament, filters[:state])
|
||||||
|
|
||||||
|
matches.select { |match| match_matches_filters?(match, filters) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_filters(filters = {}, state: nil)
|
||||||
|
raw_filters = filters.to_h.with_indifferent_access
|
||||||
|
raw_filters[:state] = state unless state.nil?
|
||||||
|
|
||||||
|
{
|
||||||
|
state: raw_filters[:state].presence,
|
||||||
|
stage_id: integer_filter(raw_filters[:stage_id]),
|
||||||
|
stage_level: integer_filter(raw_filters[:stage_level]),
|
||||||
|
position: integer_filter(raw_filters[:position]),
|
||||||
|
group_stage_position: integer_filter(raw_filters[:group_stage_position])
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state_filtered_matches(tournament, state)
|
||||||
return tournament.matches if state.blank?
|
return tournament.matches if state.blank?
|
||||||
return upcoming_matches_for(tournament) if state == 'upcoming'
|
return upcoming_matches_for(tournament) if state == 'upcoming'
|
||||||
|
|
||||||
tournament.matches.select { |match| match.state == state }
|
tournament.matches.select { |match| match.state == state }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def match_matches_filters?(match, filters)
|
||||||
|
stage = match_stage(match)
|
||||||
|
|
||||||
|
stage_id_matches?(stage, filters) &&
|
||||||
|
stage_level_matches?(stage, filters) &&
|
||||||
|
position_matches?(match, filters) &&
|
||||||
|
group_stage_position_matches?(match, stage, filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_stage(match)
|
||||||
|
match.stage || match.group&.stage
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_id_matches?(stage, filters)
|
||||||
|
filters[:stage_id].nil? || stage&.id == filters[:stage_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_level_matches?(stage, filters)
|
||||||
|
filters[:stage_level].nil? || stage&.level == filters[:stage_level]
|
||||||
|
end
|
||||||
|
|
||||||
|
def position_matches?(match, filters)
|
||||||
|
filters[:position].nil? || match.position == filters[:position]
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_stage_position_matches?(match, stage, filters)
|
||||||
|
filters[:group_stage_position].nil? ||
|
||||||
|
(match.group_match? && stage&.level == -1 && match.position == filters[:group_stage_position])
|
||||||
|
end
|
||||||
|
|
||||||
|
def integer_filter(value)
|
||||||
|
return nil if value.blank?
|
||||||
|
return value if value.is_a?(Integer)
|
||||||
|
|
||||||
|
Integer(value, 10)
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
:invalid
|
||||||
|
end
|
||||||
|
|
||||||
def upcoming_matches_for(tournament)
|
def upcoming_matches_for(tournament)
|
||||||
upcoming_group_matches(tournament).presence || upcoming_playoff_matches(tournament)
|
upcoming_group_matches(tournament).presence || upcoming_playoff_matches(tournament)
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,15 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe_matches!(tournament_id:, state: nil)
|
def subscribe_matches!(tournament_id:, **filters)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentMatchesChannel',
|
channel: 'TournamentMatchesChannel',
|
||||||
tournament_id:,
|
tournament_id:,
|
||||||
state:
|
state: filters[:state],
|
||||||
|
stage_id: filters[:stage_id],
|
||||||
|
stage_level: filters[:stage_level],
|
||||||
|
position: filters[:position],
|
||||||
|
group_stage_position: filters[:group_stage_position]
|
||||||
}.compact.to_json
|
}.compact.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,27 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
||||||
expect(match[:teams]).to all(include(:id, :name))
|
expect(match[:teams]).to all(include(:id, :name))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'filters group stage matches by position across groups' do
|
||||||
|
get :index, params: { tournament_id: @tournament.to_param, group_stage_position: 0 }
|
||||||
|
body = deserialize_response(response)
|
||||||
|
|
||||||
|
expect(body).not_to be_empty
|
||||||
|
expect(body).to all(include(position: 0))
|
||||||
|
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([-1])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on a playoff tournament' do
|
||||||
|
it 'filters matches by stage level' do
|
||||||
|
stage = @tournament.stages.find_by!(level: 1)
|
||||||
|
|
||||||
|
get :index, params: { tournament_id: @tournament.to_param, stage_level: stage.level }
|
||||||
|
body = deserialize_response(response)
|
||||||
|
|
||||||
|
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([stage.level])
|
||||||
|
expect(body.map { |match| match[:id] }).to match_array(stage.matches.map(&:id))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# TODO add test for upcoming once there is test data for a "valid" group stage
|
# TODO add test for upcoming once there is test data for a "valid" group stage
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
||||||
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' })
|
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' })
|
||||||
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||||
|
matches_group_position = anonymous_client.get(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { group_stage_position: match.fetch(:position) }
|
||||||
|
)
|
||||||
|
matches_group_stage = anonymous_client.get(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { stage_level: group_stage.fetch(:level) }
|
||||||
|
)
|
||||||
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||||
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
||||||
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||||
|
|
@ -271,6 +279,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||||
expect(matches_upcoming[:status]).to eq(200)
|
expect(matches_upcoming[:status]).to eq(200)
|
||||||
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||||
|
expect(matches_group_position[:status]).to eq(200)
|
||||||
|
expect(matches_group_position[:json].map { |item| item.dig(:stage, :level) }.uniq).to eq([-1])
|
||||||
|
expect(matches_group_position[:json]).to all(include(position: match.fetch(:position)))
|
||||||
|
expect(matches_group_stage[:status]).to eq(200)
|
||||||
|
expect(matches_group_stage[:json].count).to eq(tournament_match_count(tournament))
|
||||||
|
expect(matches_group_stage[:json].map { |item| item.dig(:stage, :id) }.uniq).to eq([group_stage.fetch(:id)])
|
||||||
expect(match_show[:status]).to eq(200)
|
expect(match_show[:status]).to eq(200)
|
||||||
expect(invalid_match_update[:status]).to eq(422)
|
expect(invalid_match_update[:status]).to eq(422)
|
||||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,20 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
|
|
||||||
tournament_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
tournament_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
filtered_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
stage_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
group_position_filter = { group_stage_position: match.fetch(:position) }
|
||||||
|
stage_filter = { stage_level: group_stage.fetch(:level) }
|
||||||
tournament_cable.subscribe_tournament!(tournament_id: tournament.fetch(:id))
|
tournament_cable.subscribe_tournament!(tournament_id: tournament.fetch(:id))
|
||||||
matches_cable.subscribe_matches!(tournament_id: tournament.fetch(:id))
|
matches_cable.subscribe_matches!(tournament_id: tournament.fetch(:id))
|
||||||
|
filtered_matches_cable.subscribe_matches!(
|
||||||
|
tournament_id: tournament.fetch(:id),
|
||||||
|
**group_position_filter
|
||||||
|
)
|
||||||
|
stage_matches_cable.subscribe_matches!(
|
||||||
|
tournament_id: tournament.fetch(:id),
|
||||||
|
**stage_filter
|
||||||
|
)
|
||||||
|
|
||||||
expect(tournament_cable.wait_for_tournament_payload!.fetch('tournament')).to(
|
expect(tournament_cable.wait_for_tournament_payload!.fetch('tournament')).to(
|
||||||
eq(json_for_api(fetch_tournament(tournament.fetch(:id))))
|
eq(json_for_api(fetch_tournament(tournament.fetch(:id))))
|
||||||
|
|
@ -38,6 +50,12 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
expect(matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
expect(matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||||
)
|
)
|
||||||
|
expect(filtered_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||||
|
)
|
||||||
|
expect(stage_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||||
|
)
|
||||||
|
|
||||||
play_match!(match.fetch(:id), first_score: 3, second_score: 1)
|
play_match!(match.fetch(:id), first_score: 3, second_score: 1)
|
||||||
|
|
||||||
|
|
@ -55,9 +73,29 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
|
|
||||||
matches_payload = wait_for_matches_payload!(matches_cable, match_id: match.fetch(:id), state: 'finished')
|
matches_payload = wait_for_matches_payload!(matches_cable, match_id: match.fetch(:id), state: 'finished')
|
||||||
expect(matches_payload.fetch('matches')).to eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
expect(matches_payload.fetch('matches')).to eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||||
|
|
||||||
|
filtered_matches_payload = wait_for_matches_payload!(
|
||||||
|
filtered_matches_cable,
|
||||||
|
match_id: match.fetch(:id),
|
||||||
|
state: 'finished'
|
||||||
|
)
|
||||||
|
expect(filtered_matches_payload.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||||
|
)
|
||||||
|
|
||||||
|
stage_matches_payload = wait_for_matches_payload!(
|
||||||
|
stage_matches_cable,
|
||||||
|
match_id: match.fetch(:id),
|
||||||
|
state: 'finished'
|
||||||
|
)
|
||||||
|
expect(stage_matches_payload.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||||
|
)
|
||||||
ensure
|
ensure
|
||||||
tournament_cable&.close
|
tournament_cable&.close
|
||||||
matches_cable&.close
|
matches_cable&.close
|
||||||
|
filtered_matches_cable&.close
|
||||||
|
stage_matches_cable&.close
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client
|
def login_client
|
||||||
|
|
@ -111,8 +149,8 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_matches(tournament_id)
|
def fetch_matches(tournament_id, params: {})
|
||||||
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches")
|
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches", params:)
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,29 @@ RSpec.describe 'Tournament live payloads' do
|
||||||
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id))
|
||||||
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'filters group stage matches by position across groups' do
|
||||||
|
tournament = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
|
||||||
|
tournament.group_stage.groups.each do |group|
|
||||||
|
group.matches.each_with_index { |match, index| match.update!(position: index) }
|
||||||
|
end
|
||||||
|
|
||||||
|
matches = described_class.matches_for(tournament, group_stage_position: 0)
|
||||||
|
|
||||||
|
expect(matches).to all(be_group_match)
|
||||||
|
expect(matches.map(&:position).uniq).to eq([0])
|
||||||
|
expect(matches.map { |match| match.group.stage.level }.uniq).to eq([-1])
|
||||||
|
expect(matches.map(&:group_id)).to match_array(tournament.group_stage.groups.map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters all matches belonging to a stage level' do
|
||||||
|
tournament = create(:stage_tournament, stage_count: 2)
|
||||||
|
stage = tournament.stages.find_by!(level: 1)
|
||||||
|
|
||||||
|
matches = described_class.matches_for(tournament, stage_level: stage.level)
|
||||||
|
|
||||||
|
expect(matches).to match_array(stage.matches)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe TournamentBroadcastPayload do
|
describe TournamentBroadcastPayload do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue