Merge branch 'codex/TUR-80-manual-team-action-lists' into 'master'
TUR-80 manual team action list creation Closes TUR-80 See merge request turniere/turniere-backend!54
This commit is contained in:
commit
84a7bc8ca9
|
|
@ -176,6 +176,7 @@ Practical rule:
|
||||||
|
|
||||||
For team action lists specifically:
|
For team action lists specifically:
|
||||||
|
|
||||||
|
- create lists explicitly through owner-facing HTTP API; do not auto-generate them from stage transitions unless product requirements say so
|
||||||
- subscribe by tournament id
|
- subscribe by tournament id
|
||||||
- channel name is `TournamentTeamActionListsChannel`
|
- channel name is `TournamentTeamActionListsChannel`
|
||||||
- broadcast full current list snapshot for that tournament
|
- broadcast full current list snapshot for that tournament
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,8 @@ class StagesController < ApplicationController
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless AddPlayoffsToTournamentAndSave.call(tournament: @stage.tournament,
|
return if AddPlayoffsToTournamentAndSave.call(tournament: @stage.tournament,
|
||||||
teams: GroupStageService.get_advancing_teams(@stage)).success?
|
teams: GroupStageService.get_advancing_teams(@stage)).success?
|
||||||
|
|
||||||
TeamActionListService.ensure_group_stage_survivor_playoff_token_list!(@stage.tournament)
|
|
||||||
return
|
|
||||||
|
|
||||||
render json: { error: 'Generating group stage failed' }, status: :unprocessable_entity
|
render json: { error: 'Generating group stage failed' }, status: :unprocessable_entity
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,29 @@
|
||||||
|
|
||||||
class TeamActionListsController < ApplicationController
|
class TeamActionListsController < ApplicationController
|
||||||
before_action :set_tournament
|
before_action :set_tournament
|
||||||
before_action :authenticate_user!, only: %i[update_item]
|
before_action :authenticate_user!, only: %i[create update_item]
|
||||||
before_action -> { require_owner! @tournament.owner }, only: %i[update_item]
|
before_action -> { require_owner! @tournament.owner }, only: %i[create update_item]
|
||||||
before_action -> { require_writable_tournament!(@tournament) }, only: %i[update_item]
|
before_action -> { require_writable_tournament!(@tournament) }, only: %i[create update_item]
|
||||||
before_action :set_team_action_item, only: %i[update_item]
|
before_action :set_team_action_item, only: %i[update_item]
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||||
|
|
||||||
|
# POST /tournaments/:tournament_id/team_action_lists
|
||||||
|
def create
|
||||||
|
team_action_list = TeamActionListService.create!(
|
||||||
|
tournament: @tournament,
|
||||||
|
name: team_action_list_params.fetch(:name),
|
||||||
|
key: team_action_list_params.fetch(:key),
|
||||||
|
action_name: team_action_list_params.fetch(:action_name),
|
||||||
|
source: team_action_list_params.fetch(:source)
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: team_action_list, status: :created
|
||||||
|
rescue TeamActionListService::InvalidSourceError, TeamActionListService::EmptySourceError => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render json: e.record.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
# PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id
|
# PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id
|
||||||
def update_item
|
def update_item
|
||||||
if TeamActionItemUpdate.call(
|
if TeamActionItemUpdate.call(
|
||||||
|
|
@ -34,4 +51,8 @@ class TeamActionListsController < ApplicationController
|
||||||
def team_action_item_params
|
def team_action_item_params
|
||||||
params.slice(:completed).permit(:completed)
|
params.slice(:completed).permit(:completed)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def team_action_list_params
|
||||||
|
params.permit(:name, :key, :action_name, :source)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ class TeamActionList < ApplicationRecord
|
||||||
|
|
||||||
validates :name, :key, :action_name, :source, presence: true
|
validates :name, :key, :action_name, :source, presence: true
|
||||||
validates :key, uniqueness: { scope: :tournament_id }
|
validates :key, uniqueness: { scope: :tournament_id }
|
||||||
|
validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
|
||||||
|
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,48 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TeamActionListService
|
class TeamActionListService
|
||||||
GROUP_STAGE_SURVIVOR_PLAYOFF_TOKEN_KEY = 'group_stage_survivor_playoff_tokens'
|
SUPPORTED_SOURCES = %w[all_tournament_teams group_stage_survivors].freeze
|
||||||
|
|
||||||
|
class InvalidSourceError < StandardError; end
|
||||||
|
class EmptySourceError < StandardError; end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def ensure_group_stage_survivor_playoff_token_list!(tournament)
|
def create!(tournament:, name:, key:, action_name:, source:)
|
||||||
group_stage = tournament.group_stage
|
teams = teams_for_source!(tournament:, source:)
|
||||||
return nil unless group_stage&.finished?
|
raise EmptySourceError, "Source #{source} does not currently contain any teams" if teams.empty?
|
||||||
|
|
||||||
teams = tournament.teams_advancing_from_group_stage
|
list = nil
|
||||||
return nil if teams.empty?
|
TeamActionList.transaction do
|
||||||
|
list = tournament.team_action_lists.new(
|
||||||
|
name:,
|
||||||
|
key:,
|
||||||
|
action_name:,
|
||||||
|
source:
|
||||||
|
)
|
||||||
|
list.save!
|
||||||
|
list.sync_teams!(teams)
|
||||||
|
end
|
||||||
|
|
||||||
ensure_list!(
|
TournamentSyncEnqueue.call(tournament)
|
||||||
tournament: tournament,
|
list
|
||||||
key: GROUP_STAGE_SURVIVOR_PLAYOFF_TOKEN_KEY,
|
|
||||||
name: 'Playoff token collection',
|
|
||||||
action_name: 'Collect playoff token',
|
|
||||||
source: 'group_stage_survivors',
|
|
||||||
teams: teams
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_list!(tournament:, key:, name:, action_name:, source:, teams:)
|
def source_keys
|
||||||
list = tournament.team_action_lists.find_or_initialize_by(key: key)
|
SUPPORTED_SOURCES
|
||||||
list.name = name
|
end
|
||||||
list.action_name = action_name
|
|
||||||
list.source = source
|
def teams_for_source!(tournament:, source:)
|
||||||
list.save!
|
case source
|
||||||
list.sync_teams!(teams)
|
when 'all_tournament_teams'
|
||||||
list
|
tournament.teams.to_a
|
||||||
|
when 'group_stage_survivors'
|
||||||
|
group_stage = tournament.group_stage
|
||||||
|
return [] unless group_stage&.finished?
|
||||||
|
|
||||||
|
tournament.teams_advancing_from_group_stage
|
||||||
|
else
|
||||||
|
raise InvalidSourceError, "Unsupported team action list source: #{source}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ Rails.application.routes.draw do
|
||||||
resources :teams, only: %i[show update]
|
resources :teams, only: %i[show update]
|
||||||
resources :team_action_items, only: %i[update]
|
resources :team_action_items, only: %i[update]
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
|
resources :team_action_lists, only: %i[create]
|
||||||
patch 'team_action_lists/:key/teams/:team_id', to: 'team_action_lists#update_item'
|
patch 'team_action_lists/:key/teams/:team_id', to: 'team_action_lists#update_item'
|
||||||
resources :statistics, only: %i[index]
|
resources :statistics, only: %i[index]
|
||||||
resources :matches, only: %i[index]
|
resources :matches, only: %i[index]
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,19 @@
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Team action lists let the backend expose checklist-style workflows for teams inside a tournament.
|
Team action lists let tournament owners create checklist-style workflows for teams inside a tournament.
|
||||||
|
|
||||||
Current example:
|
Examples:
|
||||||
|
|
||||||
- `group_stage_survivor_playoff_tokens`
|
- collect playoff tokens
|
||||||
|
- track team check-in
|
||||||
|
- hand out medals
|
||||||
|
|
||||||
That list contains all teams that advanced from the group stage and tracks whether each team already collected a playoff token.
|
Important:
|
||||||
|
|
||||||
|
- lists are not auto-generated by backend tournament events
|
||||||
|
- owner creates a list explicitly through API
|
||||||
|
- backend materializes the initial team items from the chosen `source`
|
||||||
|
|
||||||
## Read Flow
|
## Read Flow
|
||||||
|
|
||||||
|
|
@ -26,7 +32,7 @@ Example response shape:
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"name": "Playoff token collection",
|
"name": "Playoff token collection",
|
||||||
"key": "group_stage_survivor_playoff_tokens",
|
"key": "playoff_token_collection",
|
||||||
"action_name": "Collect playoff token",
|
"action_name": "Collect playoff token",
|
||||||
"source": "group_stage_survivors",
|
"source": "group_stage_survivors",
|
||||||
"team_action_items": [
|
"team_action_items": [
|
||||||
|
|
@ -45,6 +51,50 @@ Example response shape:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Create Flow
|
||||||
|
|
||||||
|
Only tournament owner may create lists.
|
||||||
|
Follower tournaments in `read_only_mode` reject creation with `423`.
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
|
||||||
|
- `POST /tournaments/:tournament_id/team_action_lists`
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Playoff token collection",
|
||||||
|
"key": "playoff_token_collection",
|
||||||
|
"action_name": "Collect playoff token",
|
||||||
|
"source": "group_stage_survivors"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
- `201 Created`
|
||||||
|
- response body is the created `TeamActionList` including `team_action_items`
|
||||||
|
|
||||||
|
Current supported sources:
|
||||||
|
|
||||||
|
- `all_tournament_teams`
|
||||||
|
- `group_stage_survivors`
|
||||||
|
|
||||||
|
Current source behavior:
|
||||||
|
|
||||||
|
- `all_tournament_teams` uses all teams currently in tournament
|
||||||
|
- `group_stage_survivors` uses teams currently advancing from a finished group stage
|
||||||
|
|
||||||
|
Creation is snapshot-style:
|
||||||
|
|
||||||
|
- backend resolves teams at create time
|
||||||
|
- backend creates one item per resolved team
|
||||||
|
- backend does not auto-create lists later
|
||||||
|
- backend does not auto-refresh existing list membership later
|
||||||
|
|
||||||
|
If selected source currently resolves to no teams, backend returns `422`.
|
||||||
|
|
||||||
## Write Flow
|
## Write Flow
|
||||||
|
|
||||||
Only tournament owner may change item status.
|
Only tournament owner may change item status.
|
||||||
|
|
@ -79,7 +129,7 @@ Endpoint:
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
- `PATCH /tournaments/123/team_action_lists/group_stage_survivor_playoff_tokens/teams/55`
|
- `PATCH /tournaments/123/team_action_lists/playoff_token_collection/teams/55`
|
||||||
|
|
||||||
Request body:
|
Request body:
|
||||||
|
|
||||||
|
|
@ -93,7 +143,12 @@ Response body for both write endpoints is the updated `TeamActionItem`.
|
||||||
|
|
||||||
## Idempotency Rules
|
## Idempotency Rules
|
||||||
|
|
||||||
These endpoints are designed to be idempotent if frontend sends explicit target state.
|
Create endpoint is idempotent by unique business key per tournament:
|
||||||
|
|
||||||
|
- backend enforces unique `(tournament_id, key)`
|
||||||
|
- repeated create with same key returns `422`, not a second list
|
||||||
|
|
||||||
|
Item update endpoints are idempotent if frontend sends explicit target state.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
|
|
||||||
|
|
@ -103,12 +158,12 @@ Good:
|
||||||
Avoid:
|
Avoid:
|
||||||
|
|
||||||
- toggle-style client behavior such as "invert current state"
|
- toggle-style client behavior such as "invert current state"
|
||||||
|
- whole-list writeback from stale client state
|
||||||
|
|
||||||
Why this is idempotent:
|
Why item updates are idempotent:
|
||||||
|
|
||||||
- sending `completed=true` multiple times leaves item in same final state
|
- sending `completed=true` multiple times leaves item in same final state
|
||||||
- sending `completed=false` multiple times leaves item in same final state
|
- sending `completed=false` multiple times leaves item in same final state
|
||||||
- backend does not create duplicate items
|
|
||||||
- backend stores one unique item per `(team_action_list_id, team_id)`
|
- backend stores one unique item per `(team_action_list_id, team_id)`
|
||||||
|
|
||||||
`completed_at` behavior:
|
`completed_at` behavior:
|
||||||
|
|
@ -122,10 +177,10 @@ Why this is idempotent:
|
||||||
Recommended frontend flow:
|
Recommended frontend flow:
|
||||||
|
|
||||||
1. Load tournament via `GET /tournaments/:id`
|
1. Load tournament via `GET /tournaments/:id`
|
||||||
2. Find desired list by stable `key`
|
2. Render current `team_action_lists`
|
||||||
3. Render `team_action_items`
|
3. If owner wants a new checklist, send `POST /tournaments/:id/team_action_lists`
|
||||||
4. On user action, send explicit target state
|
4. For item changes, send explicit target state for one item only
|
||||||
5. Update local UI from response or refetch tournament
|
5. Update local UI from response or websocket snapshot
|
||||||
|
|
||||||
Recommended frontend identifiers:
|
Recommended frontend identifiers:
|
||||||
|
|
||||||
|
|
@ -135,6 +190,7 @@ Recommended frontend identifiers:
|
||||||
|
|
||||||
Recommended optimistic UI:
|
Recommended optimistic UI:
|
||||||
|
|
||||||
|
- create list only once per intended key
|
||||||
- set checkbox immediately to requested target state
|
- set checkbox immediately to requested target state
|
||||||
- revert if API returns error
|
- revert if API returns error
|
||||||
- do not compute next state from stale cached data if multiple clients may edit
|
- do not compute next state from stale cached data if multiple clients may edit
|
||||||
|
|
@ -143,11 +199,13 @@ Recommended optimistic UI:
|
||||||
|
|
||||||
Expected statuses:
|
Expected statuses:
|
||||||
|
|
||||||
- `200` update accepted
|
- `201` list created
|
||||||
|
- `200` item update accepted
|
||||||
|
- `401` unauthenticated
|
||||||
- `403` authenticated but not tournament owner
|
- `403` authenticated but not tournament owner
|
||||||
- `404` tournament, list, team, or item not found
|
- `404` tournament, list, team, or item not found
|
||||||
|
- `422` invalid create payload, duplicate key, unsupported source, or source with no teams
|
||||||
- `423` follower tournament is read-only
|
- `423` follower tournament is read-only
|
||||||
- `401` unauthenticated
|
|
||||||
|
|
||||||
## Sync / Follower Behavior
|
## Sync / Follower Behavior
|
||||||
|
|
||||||
|
|
@ -155,10 +213,12 @@ Team action lists are included in tournament sync snapshots.
|
||||||
|
|
||||||
That means:
|
That means:
|
||||||
|
|
||||||
- source tournament owner updates item state
|
- source owner creates list on source tournament
|
||||||
- backend enqueues tournament sync
|
- backend enqueues tournament sync
|
||||||
- follower tournament receives updated list state
|
- follower tournament receives created list and all items
|
||||||
- follower users can view but not mutate item state while read-only
|
- source owner updates item state
|
||||||
|
- follower tournament receives updated completion state
|
||||||
|
- follower users can view but not mutate list state while read-only
|
||||||
|
|
||||||
## Websocket / Live Update Behavior
|
## Websocket / Live Update Behavior
|
||||||
|
|
||||||
|
|
@ -168,7 +228,7 @@ This is a websocket flow, not a webhook flow.
|
||||||
|
|
||||||
- backend pushes updates to subscribed clients over websocket
|
- backend pushes updates to subscribed clients over websocket
|
||||||
- frontend does not register callback URLs
|
- frontend does not register callback URLs
|
||||||
- writes still happen over normal HTTP `PATCH` requests
|
- writes still happen over normal HTTP `POST` and `PATCH` requests
|
||||||
|
|
||||||
Endpoint:
|
Endpoint:
|
||||||
|
|
||||||
|
|
@ -190,11 +250,12 @@ Subscription identifier example:
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- subscription immediately receives current full team-action-list snapshot for that tournament
|
- subscription immediately receives current full team-action-list snapshot for that tournament
|
||||||
- later list/item changes broadcast updated full snapshot again
|
- later list creation and item changes broadcast updated full snapshot again
|
||||||
- same broadcast path is used for:
|
- same broadcast path is used for:
|
||||||
- direct source updates
|
- list creation
|
||||||
|
- direct item updates
|
||||||
- follower sync imports
|
- follower sync imports
|
||||||
- both write endpoints trigger the same backend update path before broadcast:
|
- both item update endpoints trigger the same backend update path before broadcast:
|
||||||
- `PATCH /team_action_items/:id`
|
- `PATCH /team_action_items/:id`
|
||||||
- `PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id`
|
- `PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id`
|
||||||
|
|
||||||
|
|
@ -208,7 +269,7 @@ Payload shape:
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"name": "Playoff token collection",
|
"name": "Playoff token collection",
|
||||||
"key": "group_stage_survivor_playoff_tokens",
|
"key": "playoff_token_collection",
|
||||||
"action_name": "Collect playoff token",
|
"action_name": "Collect playoff token",
|
||||||
"source": "group_stage_survivors",
|
"source": "group_stage_survivors",
|
||||||
"team_action_items": [
|
"team_action_items": [
|
||||||
|
|
@ -239,20 +300,22 @@ Practical frontend pattern:
|
||||||
1. load initial tournament state over HTTP
|
1. load initial tournament state over HTTP
|
||||||
2. open websocket subscription for same tournament id
|
2. open websocket subscription for same tournament id
|
||||||
3. render incoming websocket payload as latest live snapshot
|
3. render incoming websocket payload as latest live snapshot
|
||||||
4. when user checks or unchecks one team, send one idempotent HTTP `PATCH` for that item only
|
4. if owner submits create form, send one HTTP `POST` to create the list
|
||||||
5. let websocket keep other viewers in sync
|
5. when user checks or unchecks one team, send one idempotent HTTP `PATCH` for that item only
|
||||||
|
6. let websocket keep other viewers in sync
|
||||||
|
|
||||||
This avoids whole-list writebacks and reduces race-condition risk between multiple clients.
|
This avoids whole-list writebacks and reduces race-condition risk between multiple clients.
|
||||||
|
|
||||||
## Future Extension Guidance
|
## Future Extension Guidance
|
||||||
|
|
||||||
When new lists are added, frontend should not hardcode backend internals beyond:
|
When new sources or list types are added, frontend should not hardcode backend internals beyond:
|
||||||
|
|
||||||
- stable list `key`
|
- stable list `key`
|
||||||
- user-facing `name`
|
- user-facing `name`
|
||||||
- user-facing `action_name`
|
- user-facing `action_name`
|
||||||
|
- selected `source`
|
||||||
|
|
||||||
New list types should automatically fit the same frontend rendering model:
|
New list types should still fit same frontend rendering model:
|
||||||
|
|
||||||
- list metadata
|
- list metadata
|
||||||
- list items
|
- list items
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,9 @@ RSpec.describe StagesController, type: :controller do
|
||||||
.to match_array(GroupStageService.get_advancing_teams(running_group_stage))
|
.to match_array(GroupStageService.get_advancing_teams(running_group_stage))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates playoff token collection list for advancing teams' do
|
it 'does not auto-create team action lists' do
|
||||||
tournament = running_group_stage.tournament.reload
|
tournament = running_group_stage.tournament.reload
|
||||||
list = tournament.team_action_lists.find_by!(key: TeamActionListService::GROUP_STAGE_SURVIVOR_PLAYOFF_TOKEN_KEY)
|
expect(tournament.team_action_lists).to be_empty
|
||||||
|
|
||||||
expect(list.name).to eq('Playoff token collection')
|
|
||||||
expect(list.team_action_items.includes(:team).map(&:team))
|
|
||||||
.to match_array(GroupStageService.get_advancing_teams(running_group_stage))
|
|
||||||
expect(list.team_action_items.pluck(:completed).uniq).to eq([false])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,113 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe TeamActionListsController, type: :controller do
|
RSpec.describe TeamActionListsController, type: :controller do
|
||||||
|
let(:tournament) { create(:tournament, teams_count: 4) }
|
||||||
let(:team_action_item) { create(:team_action_item) }
|
let(:team_action_item) { create(:team_action_item) }
|
||||||
let(:tournament) { team_action_item.tournament }
|
|
||||||
let(:list) { team_action_item.team_action_list }
|
let(:list) { team_action_item.team_action_list }
|
||||||
let(:team) { team_action_item.team }
|
let(:team) { team_action_item.team }
|
||||||
|
|
||||||
describe 'PATCH #update_item' do
|
describe 'POST #create' do
|
||||||
|
let(:create_params) do
|
||||||
|
{
|
||||||
|
tournament_id: tournament.to_param,
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: 'playoff_token_collection',
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'all_tournament_teams'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'as owner' do
|
context 'as owner' do
|
||||||
before do
|
before do
|
||||||
apply_authentication_headers_for tournament.owner
|
apply_authentication_headers_for tournament.owner
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a list populated from the selected source' do
|
||||||
|
post :create, params: create_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
created_list = tournament.team_action_lists.find_by!(key: 'playoff_token_collection')
|
||||||
|
expect(created_list.name).to eq('Playoff token collection')
|
||||||
|
expect(created_list.action_name).to eq('Collect playoff token')
|
||||||
|
expect(created_list.source).to eq('all_tournament_teams')
|
||||||
|
expect(created_list.team_action_items.includes(:team).map(&:team)).to match_array(tournament.teams)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'enqueues tournament sync after create' do
|
||||||
|
allow(TournamentSyncEnqueue).to receive(:call)
|
||||||
|
|
||||||
|
post :create, params: create_params
|
||||||
|
|
||||||
|
expect(TournamentSyncEnqueue).to have_received(:call).with(tournament)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unprocessable entity for unsupported source' do
|
||||||
|
post :create, params: create_params.merge(source: 'unknown_source')
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(deserialize_response(response)[:error]).to eq('Unsupported team action list source: unknown_source')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unprocessable entity when selected source resolves to no teams' do
|
||||||
|
empty_tournament = create(:tournament, teams_count: 0)
|
||||||
|
apply_authentication_headers_for empty_tournament.owner
|
||||||
|
|
||||||
|
post :create, params: create_params.merge(tournament_id: empty_tournament.to_param)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(deserialize_response(response)[:error]).to eq('Source all_tournament_teams does not currently contain any teams')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unprocessable entity for duplicate key' do
|
||||||
|
tournament.team_action_lists.create!(
|
||||||
|
name: 'Existing list',
|
||||||
|
key: 'playoff_token_collection',
|
||||||
|
action_name: 'Existing action',
|
||||||
|
source: 'all_tournament_teams'
|
||||||
|
)
|
||||||
|
|
||||||
|
post :create, params: create_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as another user' do
|
||||||
|
before do
|
||||||
|
apply_authentication_headers_for create(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns forbidden' do
|
||||||
|
post :create, params: create_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for follower tournament' do
|
||||||
|
before do
|
||||||
|
tournament.update!(read_only_mode: true, sync_auth_token: 'sync-token')
|
||||||
|
apply_authentication_headers_for tournament.owner
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns locked' do
|
||||||
|
post :create, params: create_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:locked)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #update_item' do
|
||||||
|
context 'as owner' do
|
||||||
|
before do
|
||||||
|
apply_authentication_headers_for list.tournament.owner
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates item by list key and team id' do
|
it 'updates item by list key and team id' do
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: list.key,
|
key: list.key,
|
||||||
team_id: team.to_param,
|
team_id: team.to_param,
|
||||||
completed: true
|
completed: true
|
||||||
|
|
@ -29,7 +122,7 @@ RSpec.describe TeamActionListsController, type: :controller do
|
||||||
it 'is idempotent when same state sent repeatedly' do
|
it 'is idempotent when same state sent repeatedly' do
|
||||||
2.times do
|
2.times do
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: list.key,
|
key: list.key,
|
||||||
team_id: team.to_param,
|
team_id: team.to_param,
|
||||||
completed: true
|
completed: true
|
||||||
|
|
@ -44,13 +137,13 @@ RSpec.describe TeamActionListsController, type: :controller do
|
||||||
allow(TournamentSyncEnqueue).to receive(:call)
|
allow(TournamentSyncEnqueue).to receive(:call)
|
||||||
|
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: list.key,
|
key: list.key,
|
||||||
team_id: team.to_param,
|
team_id: team.to_param,
|
||||||
completed: true
|
completed: true
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(TournamentSyncEnqueue).to have_received(:call).with(tournament)
|
expect(TournamentSyncEnqueue).to have_received(:call).with(list.tournament)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -61,7 +154,7 @@ RSpec.describe TeamActionListsController, type: :controller do
|
||||||
|
|
||||||
it 'returns forbidden' do
|
it 'returns forbidden' do
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: list.key,
|
key: list.key,
|
||||||
team_id: team.to_param,
|
team_id: team.to_param,
|
||||||
completed: true
|
completed: true
|
||||||
|
|
@ -73,12 +166,12 @@ RSpec.describe TeamActionListsController, type: :controller do
|
||||||
|
|
||||||
context 'when list key is missing' do
|
context 'when list key is missing' do
|
||||||
before do
|
before do
|
||||||
apply_authentication_headers_for tournament.owner
|
apply_authentication_headers_for list.tournament.owner
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns not found' do
|
it 'returns not found' do
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: 'unknown_key',
|
key: 'unknown_key',
|
||||||
team_id: team.to_param,
|
team_id: team.to_param,
|
||||||
completed: true
|
completed: true
|
||||||
|
|
@ -90,12 +183,12 @@ RSpec.describe TeamActionListsController, type: :controller do
|
||||||
|
|
||||||
context 'when team is missing from list context' do
|
context 'when team is missing from list context' do
|
||||||
before do
|
before do
|
||||||
apply_authentication_headers_for tournament.owner
|
apply_authentication_headers_for list.tournament.owner
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns not found' do
|
it 'returns not found' do
|
||||||
patch :update_item, params: {
|
patch :update_item, params: {
|
||||||
tournament_id: tournament.to_param,
|
tournament_id: list.tournament.to_param,
|
||||||
key: list.key,
|
key: list.key,
|
||||||
team_id: 999_999_999,
|
team_id: 999_999_999,
|
||||||
completed: true
|
completed: true
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
body = deserialize_response response
|
body = deserialize_response response
|
||||||
|
|
||||||
expect(body[:team_action_lists].size).to eq(1)
|
expect(body[:team_action_lists].size).to eq(1)
|
||||||
expect(body[:team_action_lists].first[:key]).to eq('group_stage_survivor_playoff_tokens')
|
expect(body[:team_action_lists].first[:key]).to eq('playoff_token_collection')
|
||||||
expect(body[:team_action_lists].first[:team_action_items].first[:completed]).to eq(true)
|
expect(body[:team_action_lists].first[:team_action_items].first[:completed]).to eq(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -141,11 +141,29 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||||
expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1)
|
expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1)
|
||||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
||||||
|
expect(source.fetch(:team_action_lists)).to eq([])
|
||||||
|
expect(follower.fetch(:team_action_lists)).to eq([])
|
||||||
|
|
||||||
|
log_phase!('create team action list on source and verify follower mirrors it')
|
||||||
|
created_list = create_team_action_list!(source.fetch(:id))
|
||||||
|
expect(created_list.fetch(:key)).to eq(playoff_token_list_key)
|
||||||
|
|
||||||
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
||||||
expect(playoff_token_list(source)).not_to be_nil
|
expect(playoff_token_list(source)).not_to be_nil
|
||||||
expect(playoff_token_list(follower)).not_to be_nil
|
expect(playoff_token_list(follower)).not_to be_nil
|
||||||
expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
||||||
.to match_array(playoff_token_list(source).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
.to match_array(playoff_token_list(source).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
||||||
|
|
||||||
|
locked_list_create = follower_owner_client.post("/tournaments/#{follower.fetch(:id)}/team_action_lists", body: {
|
||||||
|
name: 'Follower created list',
|
||||||
|
key: 'follower_created_list',
|
||||||
|
action_name: 'Should fail',
|
||||||
|
source: 'all_tournament_teams'
|
||||||
|
})
|
||||||
|
expect(locked_list_create[:status]).to eq(423)
|
||||||
|
expect(locked_list_create.dig(:json, :error)).to eq('Tournament is in read only follower mode')
|
||||||
|
|
||||||
follower_group = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }.fetch(:groups).first
|
follower_group = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }.fetch(:groups).first
|
||||||
follower_team = follower_group.fetch(:matches).first.fetch(:match_scores).first.fetch(:team)
|
follower_team = follower_group.fetch(:matches).first.fetch(:match_scores).first.fetch(:team)
|
||||||
locked_team_update = follower_owner_client.patch("/teams/#{follower_team.fetch(:id)}", body: { name: 'Nope' })
|
locked_team_update = follower_owner_client.patch("/teams/#{follower_team.fetch(:id)}", body: { name: 'Nope' })
|
||||||
|
|
@ -155,7 +173,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it')
|
log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it')
|
||||||
source_token_item = playoff_token_list(source).fetch(:team_action_items).first
|
source_token_item = playoff_token_list(source).fetch(:team_action_items).first
|
||||||
source_token_update = source_owner_client.patch(
|
source_token_update = source_owner_client.patch(
|
||||||
"/tournaments/#{source.fetch(:id)}/team_action_lists/group_stage_survivor_playoff_tokens/teams/#{source_token_item.dig(:team, :id)}",
|
"/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(:team, :id)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(source_token_update[:status]).to eq(200)
|
expect(source_token_update[:status]).to eq(200)
|
||||||
|
|
@ -167,7 +185,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name))
|
follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name))
|
||||||
locked_token_update = follower_owner_client.patch(
|
locked_token_update = follower_owner_client.patch(
|
||||||
"/tournaments/#{follower.fetch(:id)}/team_action_lists/group_stage_survivor_playoff_tokens/teams/#{follower_token_item.dig(:team, :id)}",
|
"/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(:team, :id)}",
|
||||||
body: { completed: false }
|
body: { completed: false }
|
||||||
)
|
)
|
||||||
expect(locked_token_update[:status]).to eq(423)
|
expect(locked_token_update[:status]).to eq(423)
|
||||||
|
|
@ -265,6 +283,17 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_team_action_list!(tournament_id)
|
||||||
|
response = source_owner_client.post("/tournaments/#{tournament_id}/team_action_lists", body: {
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: playoff_token_list_key,
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'group_stage_survivors'
|
||||||
|
})
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
def seed_unrelated_tournaments!(client:, prefix:, range:)
|
def seed_unrelated_tournaments!(client:, prefix:, range:)
|
||||||
amount = rand(range)
|
amount = rand(range)
|
||||||
Array.new(amount) do |index|
|
Array.new(amount) do |index|
|
||||||
|
|
@ -552,10 +581,14 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
def playoff_token_list(tournament)
|
def playoff_token_list(tournament)
|
||||||
tournament.fetch(:team_action_lists, []).find do |list|
|
tournament.fetch(:team_action_lists, []).find do |list|
|
||||||
list.fetch(:key) == 'group_stage_survivor_playoff_tokens'
|
list.fetch(:key) == playoff_token_list_key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playoff_token_list_key
|
||||||
|
'playoff_token_collection'
|
||||||
|
end
|
||||||
|
|
||||||
def find_team_action_item(tournament, team_name:)
|
def find_team_action_item(tournament, team_name:)
|
||||||
playoff_token_list(tournament).fetch(:team_action_items).find do |item|
|
playoff_token_list(tournament).fetch(:team_action_items).find do |item|
|
||||||
item.dig(:team, :name) == team_name
|
item.dig(:team, :name) == team_name
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
expect(configure_sync[:status]).to eq(200)
|
expect(configure_sync[:status]).to eq(200)
|
||||||
|
|
||||||
finish_group_stage_and_create_playoffs!(source_id: source.fetch(:id))
|
finish_group_stage_and_create_playoffs!(source_id: source.fetch(:id))
|
||||||
|
create_team_action_list!(source.fetch(:id))
|
||||||
follower = wait_for_list_presence!(follower.fetch(:id))
|
follower = wait_for_list_presence!(follower.fetch(:id))
|
||||||
|
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
||||||
|
|
@ -106,6 +107,17 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_team_action_list!(tournament_id)
|
||||||
|
response = source_client.post("/tournaments/#{tournament_id}/team_action_lists", body: {
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: playoff_token_list_key,
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'group_stage_survivors'
|
||||||
|
})
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
def finish_group_stage_and_create_playoffs!(source_id:)
|
def finish_group_stage_and_create_playoffs!(source_id:)
|
||||||
tournament = fetch_tournament(base_url: source_base_url, tournament_id: source_id)
|
tournament = fetch_tournament(base_url: source_base_url, tournament_id: source_id)
|
||||||
group_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
group_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||||
|
|
@ -161,13 +173,17 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
|
|
||||||
def find_playoff_token_list!(tournament)
|
def find_playoff_token_list!(tournament)
|
||||||
tournament.fetch(:team_action_lists).find do |list|
|
tournament.fetch(:team_action_lists).find do |list|
|
||||||
list.fetch(:key) == 'group_stage_survivor_playoff_tokens'
|
list.fetch(:key) == playoff_token_list_key
|
||||||
end || raise('playoff token list missing')
|
end || raise('playoff token list missing')
|
||||||
end
|
end
|
||||||
|
|
||||||
def team_action_item_completed?(payload, team_name:)
|
def team_action_item_completed?(payload, team_name:)
|
||||||
list = payload.fetch('team_action_lists').find { |candidate| candidate.fetch('key') == 'group_stage_survivor_playoff_tokens' }
|
list = payload.fetch('team_action_lists').find { |candidate| candidate.fetch('key') == playoff_token_list_key }
|
||||||
item = list.fetch('team_action_items').find { |candidate| candidate.dig('team', 'name') == team_name }
|
item = list.fetch('team_action_items').find { |candidate| candidate.dig('team', 'name') == team_name }
|
||||||
item.fetch('completed')
|
item.fetch('completed')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playoff_token_list_key
|
||||||
|
'playoff_token_collection'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
)
|
)
|
||||||
|
|
||||||
finish_group_stage_and_create_playoffs!(leader.fetch(:id))
|
finish_group_stage_and_create_playoffs!(leader.fetch(:id))
|
||||||
|
create_team_action_list!(leader.fetch(:id))
|
||||||
|
|
||||||
leader_after_playoffs = fetch_tournament(leader.fetch(:id))
|
leader_after_playoffs = fetch_tournament(leader.fetch(:id))
|
||||||
token_list = find_playoff_token_list!(leader_after_playoffs)
|
token_list = find_playoff_token_list!(leader_after_playoffs)
|
||||||
|
|
@ -73,7 +74,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
expect(update_by_item_id[:status]).to eq(200)
|
expect(update_by_item_id[:status]).to eq(200)
|
||||||
|
|
||||||
update_by_business_key = client.patch(
|
update_by_business_key = client.patch(
|
||||||
"/tournaments/#{leader.fetch(:id)}/team_action_lists/group_stage_survivor_playoff_tokens/teams/#{second_item.dig(:team, :id)}",
|
"/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(update_by_business_key[:status]).to eq(200)
|
expect(update_by_business_key[:status]).to eq(200)
|
||||||
|
|
@ -94,6 +95,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
leader = create_group_stage_tournament(name_prefix: 'Missing Action Resource Leader')
|
leader = create_group_stage_tournament(name_prefix: 'Missing Action Resource Leader')
|
||||||
|
|
||||||
finish_group_stage_and_create_playoffs!(leader.fetch(:id))
|
finish_group_stage_and_create_playoffs!(leader.fetch(:id))
|
||||||
|
create_team_action_list!(leader.fetch(:id))
|
||||||
|
|
||||||
leader_after_playoffs = fetch_tournament(leader.fetch(:id))
|
leader_after_playoffs = fetch_tournament(leader.fetch(:id))
|
||||||
token_list = find_playoff_token_list!(leader_after_playoffs)
|
token_list = find_playoff_token_list!(leader_after_playoffs)
|
||||||
|
|
@ -109,7 +111,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
expect(missing_list_update[:status]).to eq(404)
|
expect(missing_list_update[:status]).to eq(404)
|
||||||
|
|
||||||
missing_team_update = client.patch(
|
missing_team_update = client.patch(
|
||||||
"/tournaments/#{leader.fetch(:id)}/team_action_lists/group_stage_survivor_playoff_tokens/teams/999999999",
|
"/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/999999999",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(missing_team_update[:status]).to eq(404)
|
expect(missing_team_update[:status]).to eq(404)
|
||||||
|
|
@ -179,6 +181,17 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
expect(finish_stage[:status]).to eq(200)
|
expect(finish_stage[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_team_action_list!(tournament_id)
|
||||||
|
response = client.post("/tournaments/#{tournament_id}/team_action_lists", body: {
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: playoff_token_list_key,
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'group_stage_survivors'
|
||||||
|
})
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
def play_match!(match_id, first_score:, second_score:)
|
def play_match!(match_id, first_score:, second_score:)
|
||||||
match_response = anonymous_client.get("/matches/#{match_id}")
|
match_response = anonymous_client.get("/matches/#{match_id}")
|
||||||
expect(match_response[:status]).to eq(200)
|
expect(match_response[:status]).to eq(200)
|
||||||
|
|
@ -198,10 +211,14 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
|
|
||||||
def find_playoff_token_list!(tournament)
|
def find_playoff_token_list!(tournament)
|
||||||
tournament.fetch(:team_action_lists).find do |list|
|
tournament.fetch(:team_action_lists).find do |list|
|
||||||
list.fetch(:key) == 'group_stage_survivor_playoff_tokens'
|
list.fetch(:key) == playoff_token_list_key
|
||||||
end || raise('playoff token list missing')
|
end || raise('playoff token list missing')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playoff_token_list_key
|
||||||
|
'playoff_token_collection'
|
||||||
|
end
|
||||||
|
|
||||||
def playoff_token_item_completed?(tournament, team_name:)
|
def playoff_token_item_completed?(tournament, team_name:)
|
||||||
list = find_playoff_token_list!(tournament)
|
list = find_playoff_token_list!(tournament)
|
||||||
item = list.fetch(:team_action_items).find { |candidate| candidate.dig(:team, :name) == team_name }
|
item = list.fetch(:team_action_items).find { |candidate| candidate.dig(:team, :name) == team_name }
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
it 'broadcasts initial and updated team action list state over websocket' do
|
it 'broadcasts initial and updated team action list state over websocket' do
|
||||||
tournament = create_group_stage_tournament(name_prefix: 'Ws Leader')
|
tournament = create_group_stage_tournament(name_prefix: 'Ws Leader')
|
||||||
finish_group_stage_and_create_playoffs!(tournament.fetch(:id))
|
finish_group_stage_and_create_playoffs!(tournament.fetch(:id))
|
||||||
|
create_team_action_list!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament(tournament.fetch(:id))
|
tournament = fetch_tournament(tournament.fetch(:id))
|
||||||
token_list = find_playoff_token_list!(tournament)
|
token_list = find_playoff_token_list!(tournament)
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id))
|
cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id))
|
||||||
|
|
||||||
initial_payload = cable_client.wait_for_payload!
|
initial_payload = cable_client.wait_for_payload!
|
||||||
expect(initial_payload.fetch('team_action_lists').first.fetch('key')).to eq('group_stage_survivor_playoff_tokens')
|
expect(initial_payload.fetch('team_action_lists').first.fetch('key')).to eq(playoff_token_list_key)
|
||||||
|
|
||||||
first_item = token_list.fetch(:team_action_items).first
|
first_item = token_list.fetch(:team_action_items).first
|
||||||
second_item = token_list.fetch(:team_action_items)[1]
|
second_item = token_list.fetch(:team_action_items)[1]
|
||||||
|
|
@ -49,7 +50,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true)
|
expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true)
|
||||||
|
|
||||||
update_by_business_key = client.patch(
|
update_by_business_key = client.patch(
|
||||||
"/tournaments/#{tournament.fetch(:id)}/team_action_lists/group_stage_survivor_playoff_tokens/teams/#{second_item.dig(:team, :id)}",
|
"/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(update_by_business_key[:status]).to eq(200)
|
expect(update_by_business_key[:status]).to eq(200)
|
||||||
|
|
@ -113,6 +114,17 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
expect(finish_stage[:status]).to eq(200)
|
expect(finish_stage[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_team_action_list!(tournament_id)
|
||||||
|
response = client.post("/tournaments/#{tournament_id}/team_action_lists", body: {
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: playoff_token_list_key,
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'group_stage_survivors'
|
||||||
|
})
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
def play_match!(match_id, first_score:, second_score:)
|
def play_match!(match_id, first_score:, second_score:)
|
||||||
match_response = anonymous_client.get("/matches/#{match_id}")
|
match_response = anonymous_client.get("/matches/#{match_id}")
|
||||||
expect(match_response[:status]).to eq(200)
|
expect(match_response[:status]).to eq(200)
|
||||||
|
|
@ -142,13 +154,17 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
|
|
||||||
def find_playoff_token_list!(tournament)
|
def find_playoff_token_list!(tournament)
|
||||||
tournament.fetch(:team_action_lists).find do |list|
|
tournament.fetch(:team_action_lists).find do |list|
|
||||||
list.fetch(:key) == 'group_stage_survivor_playoff_tokens'
|
list.fetch(:key) == playoff_token_list_key
|
||||||
end || raise('playoff token list missing')
|
end || raise('playoff token list missing')
|
||||||
end
|
end
|
||||||
|
|
||||||
def team_action_item_completed?(payload, team_name:)
|
def team_action_item_completed?(payload, team_name:)
|
||||||
list = payload.fetch('team_action_lists').find { |candidate| candidate.fetch('key') == 'group_stage_survivor_playoff_tokens' }
|
list = payload.fetch('team_action_lists').find { |candidate| candidate.fetch('key') == playoff_token_list_key }
|
||||||
item = list.fetch('team_action_items').find { |candidate| candidate.dig('team', 'name') == team_name }
|
item = list.fetch('team_action_items').find { |candidate| candidate.dig('team', 'name') == team_name }
|
||||||
item.fetch('completed')
|
item.fetch('completed')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playoff_token_list_key
|
||||||
|
'playoff_token_collection'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ FactoryBot.define do
|
||||||
factory :team_action_list do
|
factory :team_action_list do
|
||||||
tournament
|
tournament
|
||||||
name { 'Playoff token collection' }
|
name { 'Playoff token collection' }
|
||||||
key { 'group_stage_survivor_playoff_tokens' }
|
key { 'playoff_token_collection' }
|
||||||
action_name { 'Collect playoff token' }
|
action_name { 'Collect playoff token' }
|
||||||
source { 'group_stage_survivors' }
|
source { 'all_tournament_teams' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,17 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe TeamActionListsController, type: :routing do
|
RSpec.describe TeamActionListsController, type: :routing do
|
||||||
describe 'routing' do
|
describe 'routing' do
|
||||||
|
it 'routes to #create via POST' do
|
||||||
|
expect(post: '/tournaments/1/team_action_lists')
|
||||||
|
.to route_to('team_action_lists#create', tournament_id: '1')
|
||||||
|
end
|
||||||
|
|
||||||
it 'routes to #update_item via PATCH' do
|
it 'routes to #update_item via PATCH' do
|
||||||
expect(patch: '/tournaments/1/team_action_lists/group_stage_survivor_playoff_tokens/teams/2')
|
expect(patch: '/tournaments/1/team_action_lists/playoff_token_collection/teams/2')
|
||||||
.to route_to(
|
.to route_to(
|
||||||
'team_action_lists#update_item',
|
'team_action_lists#update_item',
|
||||||
tournament_id: '1',
|
tournament_id: '1',
|
||||||
key: 'group_stage_survivor_playoff_tokens',
|
key: 'playoff_token_collection',
|
||||||
team_id: '2'
|
team_id: '2'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe TeamActionListService do
|
||||||
|
describe '.create!' do
|
||||||
|
let(:tournament) { create(:tournament, teams_count: 4) }
|
||||||
|
|
||||||
|
it 'creates a list with items for all tournament teams' do
|
||||||
|
list = described_class.create!(
|
||||||
|
tournament:,
|
||||||
|
name: 'Team check-in',
|
||||||
|
key: 'team_check_in',
|
||||||
|
action_name: 'Check in',
|
||||||
|
source: 'all_tournament_teams'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(list.team_action_items.includes(:team).map(&:team)).to match_array(tournament.teams)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a list for group stage survivors when available' do
|
||||||
|
group_stage_tournament = create(:tournament, teams_count: 0, playoff_teams_amount: 2, instant_finalists_amount: 2)
|
||||||
|
group_stage = create(:group_stage, tournament: group_stage_tournament, group_count: 0)
|
||||||
|
group = create(:group, stage: group_stage, match_count: 0, number: 1)
|
||||||
|
team_a = create(:team, tournament: group_stage_tournament, name: 'A')
|
||||||
|
team_b = create(:team, tournament: group_stage_tournament, name: 'B')
|
||||||
|
create(:group_score, group:, team: team_a, group_points: 6, scored_points: 6, received_points: 0, position: 1)
|
||||||
|
create(:group_score, group:, team: team_b, group_points: 3, scored_points: 3, received_points: 3, position: 2)
|
||||||
|
group_stage.update!(state: 'finished')
|
||||||
|
|
||||||
|
list = described_class.create!(
|
||||||
|
tournament: group_stage_tournament,
|
||||||
|
name: 'Playoff token collection',
|
||||||
|
key: 'playoff_token_collection',
|
||||||
|
action_name: 'Collect playoff token',
|
||||||
|
source: 'group_stage_survivors'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(list.team_action_items.includes(:team).map(&:team)).to match_array([team_a, team_b])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises for unsupported source' do
|
||||||
|
expect do
|
||||||
|
described_class.create!(
|
||||||
|
tournament:,
|
||||||
|
name: 'Broken',
|
||||||
|
key: 'broken',
|
||||||
|
action_name: 'Break',
|
||||||
|
source: 'unknown_source'
|
||||||
|
)
|
||||||
|
end.to raise_error(TeamActionListService::InvalidSourceError, 'Unsupported team action list source: unknown_source')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when source resolves to no teams' do
|
||||||
|
empty_tournament = create(:tournament, teams_count: 0)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.create!(
|
||||||
|
tournament: empty_tournament,
|
||||||
|
name: 'Empty',
|
||||||
|
key: 'empty',
|
||||||
|
action_name: 'Nothing',
|
||||||
|
source: 'all_tournament_teams'
|
||||||
|
)
|
||||||
|
end.to raise_error(TeamActionListService::EmptySourceError, 'Source all_tournament_teams does not currently contain any teams')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue