refactor: rename timer API field to timestamp
This commit is contained in:
parent
8d882ad763
commit
69bd43b288
|
|
@ -173,14 +173,14 @@ class TournamentsController < ApplicationController
|
||||||
|
|
||||||
def timer_end_params
|
def timer_end_params
|
||||||
{
|
{
|
||||||
timer_end: params[:timer_end],
|
timer_end: params[:timer_end] || params[:timestamp],
|
||||||
timer_mode: params[:timer_mode]
|
timer_mode: params[:timer_mode]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_timer_state(tournament)
|
def serialize_timer_state(tournament)
|
||||||
{
|
{
|
||||||
timer_end: tournament.timer_end&.iso8601,
|
timestamp: tournament.timer_end&.iso8601,
|
||||||
timer_mode: tournament.timer_mode
|
timer_mode: tournament.timer_mode
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
@ -333,14 +333,17 @@ class TournamentsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_set_timer_end_params
|
def validate_set_timer_end_params
|
||||||
timer_end = params[:timer_end]
|
timestamp = params[:timestamp]
|
||||||
timer_end_seconds = params[:timer_end_seconds]
|
timestamp_seconds = params[:timestamp_seconds]
|
||||||
timer_mode = params[:timer_mode]
|
timer_mode = params[:timer_mode]
|
||||||
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
|
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
|
||||||
|
|
||||||
|
timestamp = params[:timer_end] if timestamp.blank? && params[:timer_end].present?
|
||||||
|
timestamp_seconds = params[:timer_end_seconds] if timestamp_seconds.blank? && params[:timer_end_seconds].present?
|
||||||
|
|
||||||
if clear_timer
|
if clear_timer
|
||||||
if timer_end.present? || timer_end_seconds.present? || timer_mode.present?
|
if timestamp.present? || timestamp_seconds.present? || timer_mode.present?
|
||||||
return render json: { error: 'clear_timer cannot be combined with timer values or timer_mode' },
|
return render json: { error: 'clear_timer cannot be combined with timestamp values or timer_mode' },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -353,48 +356,48 @@ class TournamentsController < ApplicationController
|
||||||
return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
# throw error if both timer_end and timer_end_seconds are present
|
# throw error if both timestamp and timestamp_seconds are present
|
||||||
if timer_end.present? && timer_end_seconds.present?
|
if timestamp.present? && timestamp_seconds.present?
|
||||||
return render json: { error: 'Only one of timer_end or timer_end_seconds is allowed' }, status: :unprocessable_entity
|
return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if timer_mode == 'countup' && timer_end_seconds.present?
|
if timer_mode == 'countup' && timestamp_seconds.present?
|
||||||
return render json: { error: 'timer_end_seconds is only allowed for countdown mode' }, status: :unprocessable_entity
|
return render json: { error: 'timestamp_seconds is only allowed for countdown mode' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if timer_end_seconds.present?
|
if timestamp_seconds.present?
|
||||||
begin
|
begin
|
||||||
timer_end_seconds = Integer(timer_end_seconds)
|
timestamp_seconds = Integer(timestamp_seconds)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
return render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity if timer_end_seconds <= 0
|
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity if timestamp_seconds <= 0
|
||||||
|
|
||||||
parsed_time = Time.zone.now + timer_end_seconds
|
parsed_time = Time.zone.now + timestamp_seconds
|
||||||
params[:timer_end] = parsed_time
|
params[:timer_end] = parsed_time
|
||||||
params[:timer_mode] = 'countdown'
|
params[:timer_mode] = 'countdown'
|
||||||
elsif timer_end.present?
|
elsif timestamp.present?
|
||||||
begin
|
begin
|
||||||
parsed_time = Time.zone.parse(timer_end)
|
parsed_time = Time.zone.parse(timestamp)
|
||||||
if parsed_time.nil?
|
if parsed_time.nil?
|
||||||
return render json: { error: 'Invalid timer_end format' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||||
elsif timer_mode != 'countup' && !parsed_time.future?
|
elsif timer_mode != 'countup' && !parsed_time.future?
|
||||||
return render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity
|
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
params[:timer_end] = parsed_time
|
params[:timer_end] = parsed_time
|
||||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return render json: { error: 'Invalid timer_end format' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
elsif timer_mode == 'countup'
|
elsif timer_mode == 'countup'
|
||||||
params[:timer_end] = Time.zone.now
|
params[:timer_end] = Time.zone.now
|
||||||
elsif timer_mode.present?
|
elsif timer_mode.present?
|
||||||
return render json: { error: 'timer_mode requires timer_end, timer_end_seconds, or clear_timer' },
|
return render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
else
|
else
|
||||||
return render json: { error: 'timer_end, timer_end_seconds, or clear_timer is required' },
|
return render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class TournamentSerializer < SimpleTournamentSerializer
|
||||||
has_many :stages
|
has_many :stages
|
||||||
has_many :team_action_lists
|
has_many :team_action_lists
|
||||||
|
|
||||||
attribute :timer_end do
|
attribute :timestamp do
|
||||||
object.timer_end&.iso8601
|
object.timer_end&.iso8601
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class TournamentSnapshotBuilder
|
||||||
|
|
||||||
def build
|
def build
|
||||||
payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES)
|
payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES)
|
||||||
payload[:timer_end] = tournament.timer_end&.iso8601
|
payload[:timestamp] = tournament.timer_end&.iso8601
|
||||||
payload[:timer_mode] = tournament.timer_mode
|
payload[:timer_mode] = tournament.timer_mode
|
||||||
payload[:sync_generated_at] = Time.current.iso8601(6)
|
payload[:sync_generated_at] = Time.current.iso8601(6)
|
||||||
payload[:teams] = teams_payload
|
payload[:teams] = teams_payload
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class TournamentSnapshotImporter
|
||||||
def update_tournament!
|
def update_tournament!
|
||||||
root_attrs = snapshot.slice(*ROOT_ATTRIBUTES)
|
root_attrs = snapshot.slice(*ROOT_ATTRIBUTES)
|
||||||
root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code])
|
root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code])
|
||||||
root_attrs[:timer_end] = parse_time(snapshot[:timer_end])
|
root_attrs[:timer_end] = parse_time(snapshot[:timestamp] || snapshot[:timer_end])
|
||||||
root_attrs[:timer_mode] = snapshot[:timer_mode]
|
root_attrs[:timer_mode] = snapshot[:timer_mode]
|
||||||
root_attrs[:sync_source_tournament_id] = snapshot[:id]
|
root_attrs[:sync_source_tournament_id] = snapshot[:id]
|
||||||
root_attrs[:sync_last_imported_snapshot_at] = snapshot_generated_at
|
root_attrs[:sync_last_imported_snapshot_at] = snapshot_generated_at
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class TournamentTimerBroadcastPayload
|
||||||
{
|
{
|
||||||
type: 'timer.updated',
|
type: 'timer.updated',
|
||||||
tournament_id: tournament.id,
|
tournament_id: tournament.id,
|
||||||
timer_end: tournament.timer_end&.iso8601,
|
timestamp: tournament.timer_end&.iso8601,
|
||||||
timer_mode: tournament.timer_mode
|
timer_mode: tournament.timer_mode
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ Backend timer supports:
|
||||||
- live websocket updates
|
- live websocket updates
|
||||||
- follower sync propagation
|
- follower sync propagation
|
||||||
|
|
||||||
Frontend should treat timer state as small state machine:
|
Frontend should treat timer state as:
|
||||||
|
|
||||||
- no timer: `timer_end = null`, `timer_mode = null`
|
- no timer: `timestamp = null`, `timer_mode = null`
|
||||||
- countdown: `timer_mode = "countdown"`
|
- countdown: `timer_mode = "countdown"`
|
||||||
- countup: `timer_mode = "countup"`
|
- countup: `timer_mode = "countup"`
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ Response shape:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timer_end": "2026-04-24T10:15:00Z",
|
"timestamp": "2026-04-24T10:15:00Z",
|
||||||
"timer_mode": "countdown"
|
"timer_mode": "countdown"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -41,23 +41,23 @@ Possible values:
|
||||||
- `timer_mode = "countup"`
|
- `timer_mode = "countup"`
|
||||||
- `timer_mode = null`
|
- `timer_mode = null`
|
||||||
|
|
||||||
## Meaning Of `timer_end`
|
## Meaning Of `timestamp`
|
||||||
|
|
||||||
Name old. Semantics now depend on mode.
|
Same field. Mode decides meaning.
|
||||||
|
|
||||||
### Countdown mode
|
### Countdown mode
|
||||||
|
|
||||||
`timer_end` means exact wall-clock timestamp when countdown reaches zero.
|
`timestamp` means exact wall-clock timestamp when countdown reaches zero.
|
||||||
|
|
||||||
Frontend math:
|
Frontend math:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
remaining_ms = max(0, timer_end - now)
|
remaining_ms = max(0, timestamp - now)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Countup mode
|
### Countup mode
|
||||||
|
|
||||||
`timer_end` means anchor timestamp when countup started.
|
`timestamp` means anchor timestamp when countup started.
|
||||||
|
|
||||||
It is **not** finish time in this mode.
|
It is **not** finish time in this mode.
|
||||||
Think of it as:
|
Think of it as:
|
||||||
|
|
@ -68,23 +68,23 @@ Think of it as:
|
||||||
Frontend math:
|
Frontend math:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
elapsed_ms = max(0, now - timer_end)
|
elapsed_ms = max(0, now - timestamp)
|
||||||
```
|
```
|
||||||
|
|
||||||
Practical example:
|
Practical example:
|
||||||
|
|
||||||
- backend returns:
|
- backend returns:
|
||||||
- `timer_mode = "countup"`
|
- `timer_mode = "countup"`
|
||||||
- `timer_end = "2026-04-24T10:00:00Z"`
|
- `timestamp = "2026-04-24T10:00:00Z"`
|
||||||
- current time:
|
- current time:
|
||||||
- `2026-04-24T10:03:15Z"`
|
- `2026-04-24T10:03:15Z`
|
||||||
|
|
||||||
Frontend should display about `03:15` elapsed.
|
Frontend should display about `03:15` elapsed.
|
||||||
|
|
||||||
### Unset timer
|
### Unset timer
|
||||||
|
|
||||||
- `timer_mode = null`
|
- `timer_mode = null`
|
||||||
- `timer_end = null`
|
- `timestamp = null`
|
||||||
|
|
||||||
Frontend should render no active timer.
|
Frontend should render no active timer.
|
||||||
|
|
||||||
|
|
@ -97,13 +97,16 @@ Endpoint:
|
||||||
|
|
||||||
- `PATCH /tournaments/:id/set_timer_end`
|
- `PATCH /tournaments/:id/set_timer_end`
|
||||||
|
|
||||||
|
Current route name still says `set_timer_end`.
|
||||||
|
Payload field name is now `timestamp`.
|
||||||
|
|
||||||
### Start countdown by duration
|
### Start countdown by duration
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timer_end_seconds": 120
|
"timestamp_seconds": 120
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -112,7 +115,7 @@ Behavior:
|
||||||
- backend computes `Time.zone.now + 120`
|
- backend computes `Time.zone.now + 120`
|
||||||
- backend stores:
|
- backend stores:
|
||||||
- `timer_mode = "countdown"`
|
- `timer_mode = "countdown"`
|
||||||
- `timer_end = computed future timestamp`
|
- `timestamp = computed future timestamp`
|
||||||
|
|
||||||
### Start countdown by explicit timestamp
|
### Start countdown by explicit timestamp
|
||||||
|
|
||||||
|
|
@ -120,7 +123,7 @@ Request:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timer_end": "2026-04-24T10:15:00Z"
|
"timestamp": "2026-04-24T10:15:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -128,7 +131,7 @@ Optional explicit mode:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timer_end": "2026-04-24T10:15:00Z",
|
"timestamp": "2026-04-24T10:15:00Z",
|
||||||
"timer_mode": "countdown"
|
"timer_mode": "countdown"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -145,10 +148,10 @@ Request:
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- backend stores current server time into `timer_end`
|
- backend stores current server time into `timestamp`
|
||||||
- backend stores `timer_mode = "countup"`
|
- backend stores `timer_mode = "countup"`
|
||||||
|
|
||||||
Frontend should interpret returned `timer_end` as start time anchor.
|
Frontend should interpret returned `timestamp` as start time anchor.
|
||||||
|
|
||||||
### Restore countup from explicit anchor
|
### Restore countup from explicit anchor
|
||||||
|
|
||||||
|
|
@ -156,7 +159,7 @@ Backend also accepts explicit timestamp with countup mode:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timer_end": "2026-04-24T10:00:00Z",
|
"timestamp": "2026-04-24T10:00:00Z",
|
||||||
"timer_mode": "countup"
|
"timer_mode": "countup"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -181,17 +184,17 @@ Behavior:
|
||||||
|
|
||||||
- backend clears both fields
|
- backend clears both fields
|
||||||
- response returns:
|
- response returns:
|
||||||
- `timer_end = null`
|
- `timestamp = null`
|
||||||
- `timer_mode = null`
|
- `timer_mode = null`
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
|
|
||||||
Backend rejects:
|
Backend rejects:
|
||||||
|
|
||||||
- both `timer_end` and `timer_end_seconds`
|
- both `timestamp` and `timestamp_seconds`
|
||||||
- invalid `timer_mode`
|
- invalid `timer_mode`
|
||||||
- `timer_end_seconds <= 0`
|
- `timestamp_seconds <= 0`
|
||||||
- `timer_end_seconds` together with `timer_mode = "countup"`
|
- `timestamp_seconds` together with `timer_mode = "countup"`
|
||||||
- `timer_mode` by itself unless it is `countup`
|
- `timer_mode` by itself unless it is `countup`
|
||||||
- `clear_timer` mixed with timer values
|
- `clear_timer` mixed with timer values
|
||||||
|
|
||||||
|
|
@ -222,7 +225,7 @@ Payload:
|
||||||
{
|
{
|
||||||
"type": "timer.updated",
|
"type": "timer.updated",
|
||||||
"tournament_id": 123,
|
"tournament_id": 123,
|
||||||
"timer_end": "2026-04-24T10:15:00Z",
|
"timestamp": "2026-04-24T10:15:00Z",
|
||||||
"timer_mode": "countdown"
|
"timer_mode": "countdown"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -243,35 +246,35 @@ Frontend rule:
|
||||||
Recommended flow:
|
Recommended flow:
|
||||||
|
|
||||||
1. Load tournament via `GET /tournaments/:id`
|
1. Load tournament via `GET /tournaments/:id`
|
||||||
2. Read `timer_mode` and `timer_end`
|
2. Read `timer_mode` and `timestamp`
|
||||||
3. Open ActionCable subscription for same tournament id
|
3. Open ActionCable subscription for same tournament id
|
||||||
4. Replace local timer state whenever `timer.updated` arrives
|
4. Replace local timer state whenever `timer.updated` arrives
|
||||||
5. Derive displayed time from `Date.now()` plus stored server timestamp
|
5. Derive displayed time from `Date.now()` plus stored backend timestamp
|
||||||
|
|
||||||
Recommended client state:
|
Recommended client state:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type TimerState =
|
type TimerState =
|
||||||
| { mode: null; anchorIso: null }
|
| { mode: null; timestampIso: null }
|
||||||
| { mode: "countdown"; anchorIso: string }
|
| { mode: "countdown"; timestampIso: string }
|
||||||
| { mode: "countup"; anchorIso: string };
|
| { mode: "countup"; timestampIso: string };
|
||||||
```
|
```
|
||||||
|
|
||||||
Suggested derivation logic:
|
Suggested derivation logic:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function getTimerDisplay(state: TimerState, nowMs: number) {
|
function getTimerDisplay(state: TimerState, nowMs: number) {
|
||||||
if (state.mode === null || state.anchorIso === null) {
|
if (state.mode === null || state.timestampIso === null) {
|
||||||
return { active: false, ms: 0 };
|
return { active: false, ms: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorMs = new Date(state.anchorIso).getTime();
|
const timestampMs = new Date(state.timestampIso).getTime();
|
||||||
|
|
||||||
if (state.mode === "countdown") {
|
if (state.mode === "countdown") {
|
||||||
return { active: true, ms: Math.max(0, anchorMs - nowMs) };
|
return { active: true, ms: Math.max(0, timestampMs - nowMs) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { active: true, ms: Math.max(0, nowMs - anchorMs) };
|
return { active: true, ms: Math.max(0, nowMs - timestampMs) };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -284,13 +287,13 @@ Recommended rendering:
|
||||||
Recommended local ticking:
|
Recommended local ticking:
|
||||||
|
|
||||||
- keep backend state as timestamp + mode
|
- keep backend state as timestamp + mode
|
||||||
- update rendered value with 250ms or 1s local interval
|
- update rendered value with local interval
|
||||||
- do not poll backend every second
|
- do not poll backend every second
|
||||||
|
|
||||||
Recommended write UX:
|
Recommended write UX:
|
||||||
|
|
||||||
- owner actions send explicit target request
|
- owner actions send explicit target request
|
||||||
- after successful response, update local state from response body
|
- after success, update local state from response body
|
||||||
- websocket should converge all viewers to same state
|
- websocket should converge all viewers to same state
|
||||||
|
|
||||||
## Follower Behavior
|
## Follower Behavior
|
||||||
|
|
@ -300,7 +303,7 @@ Timer state is included in tournament sync snapshots.
|
||||||
That means:
|
That means:
|
||||||
|
|
||||||
- leader timer changes propagate to follower
|
- leader timer changes propagate to follower
|
||||||
- follower tournament exposes same `timer_end` / `timer_mode`
|
- follower tournament exposes same `timestamp` / `timer_mode`
|
||||||
- follower websocket subscribers receive `timer.updated`
|
- follower websocket subscribers receive `timer.updated`
|
||||||
- follower still rejects timer writes while read only
|
- follower still rejects timer writes while read only
|
||||||
|
|
||||||
|
|
@ -322,4 +325,4 @@ Implemented coverage:
|
||||||
Current note:
|
Current note:
|
||||||
|
|
||||||
- single-backend HTTP E2E timer specs pass
|
- single-backend HTTP E2E timer specs pass
|
||||||
- follower blackbox task still has environment/network issue in existing Docker follow setup, so production-style follow stack verification is not green yet
|
- follower blackbox task still has existing Docker/network issue in follow setup, so production-style follow stack verification is not green yet
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,9 @@ module TurniereE2E
|
||||||
|
|
||||||
timer_response = patch!(
|
timer_response = patch!(
|
||||||
"/tournaments/#{tournament.fetch(:id)}/set_timer_end",
|
"/tournaments/#{tournament.fetch(:id)}/set_timer_end",
|
||||||
body: { timer_end_seconds: 600 }
|
body: { timestamp_seconds: 600 }
|
||||||
)
|
)
|
||||||
checkpoints[:timer_set] = { timer_end: timer_response.fetch(:timer_end) }
|
checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) }
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set
|
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set
|
||||||
|
|
||||||
upcoming_matches = get!(
|
upcoming_matches = get!(
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
expected_keys = %i[
|
expected_keys = %i[
|
||||||
id name code public description playoff_teams_amount
|
id name code public description playoff_teams_amount
|
||||||
instant_finalists_amount intermediate_round_participants_amount
|
instant_finalists_amount intermediate_round_participants_amount
|
||||||
timer_end timer_mode owner_username stages teams team_action_lists
|
timestamp timer_mode owner_username stages teams team_action_lists
|
||||||
]
|
]
|
||||||
expect(json.keys).to match_array(expected_keys)
|
expect(json.keys).to match_array(expected_keys)
|
||||||
expect(json[:name]).to eq(@tournament.name)
|
expect(json[:name]).to eq(@tournament.name)
|
||||||
|
|
@ -228,7 +228,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
get :timer_end, params: { id: @tournament.to_param }
|
get :timer_end, params: { id: @tournament.to_param }
|
||||||
|
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
expect(deserialize_response(response)[:timer_end]).to eq(timer_end.iso8601)
|
expect(deserialize_response(response)[:timestamp]).to eq(timer_end.iso8601)
|
||||||
expect(deserialize_response(response)[:timer_mode]).to eq('countdown')
|
expect(deserialize_response(response)[:timer_mode]).to eq('countdown')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -238,14 +238,14 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
apply_authentication_headers_for @user
|
apply_authentication_headers_for @user
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'stores timezone-aware timer_end values without shifting them' do
|
it 'stores timezone-aware timestamp values without shifting them' do
|
||||||
timer_end = '2030-04-13T18:00:00+02:00'
|
timestamp = '2030-04-13T18:00:00+02:00'
|
||||||
|
|
||||||
patch :set_timer_end, params: { id: @tournament.to_param, timer_end: timer_end }
|
patch :set_timer_end, params: { id: @tournament.to_param, timestamp: timestamp }
|
||||||
|
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
expect(@tournament.reload.timer_end.iso8601).to eq(Time.iso8601(timer_end).utc.iso8601)
|
expect(@tournament.reload.timer_end.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601)
|
||||||
expect(deserialize_response(response)[:timer_end]).to eq(@tournament.timer_end.iso8601)
|
expect(deserialize_response(response)[:timestamp]).to eq(@tournament.timer_end.iso8601)
|
||||||
expect(deserialize_response(response)[:timer_mode]).to eq('countdown')
|
expect(deserialize_response(response)[:timer_mode]).to eq('countdown')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -268,15 +268,15 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
expect(@tournament.reload.timer_end).to be_nil
|
expect(@tournament.reload.timer_end).to be_nil
|
||||||
expect(@tournament.timer_mode).to be_nil
|
expect(@tournament.timer_mode).to be_nil
|
||||||
expect(deserialize_response(response)[:timer_end]).to be_nil
|
expect(deserialize_response(response)[:timestamp]).to be_nil
|
||||||
expect(deserialize_response(response)[:timer_mode]).to be_nil
|
expect(deserialize_response(response)[:timer_mode]).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects timer_end_seconds for countup mode' do
|
it 'rejects timestamp_seconds for countup mode' do
|
||||||
patch :set_timer_end, params: { id: @tournament.to_param, timer_mode: 'countup', timer_end_seconds: 120 }
|
patch :set_timer_end, params: { id: @tournament.to_param, timer_mode: 'countup', timestamp_seconds: 120 }
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(deserialize_response(response)[:error]).to eq('timer_end_seconds is only allowed for countdown mode')
|
expect(deserialize_response(response)[:error]).to eq('timestamp_seconds is only allowed for countdown mode')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,21 +125,21 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: 'Not Allowed' })
|
forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: 'Not Allowed' })
|
||||||
unauthenticated_update = anonymous_client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: 'No Auth' })
|
unauthenticated_update = anonymous_client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: 'No Auth' })
|
||||||
|
|
||||||
timer_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 120 })
|
timer_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 120 })
|
||||||
timer_show = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/timer_end")
|
timer_show = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/timer_end")
|
||||||
timer_countup = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
timer_countup = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
||||||
timer_clear = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
timer_clear = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
||||||
timer_double = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: {
|
timer_double = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: {
|
||||||
timer_end_seconds: 120,
|
timestamp_seconds: 120,
|
||||||
timer_end: (Time.now.utc + 240).iso8601
|
timestamp: (Time.now.utc + 240).iso8601
|
||||||
})
|
})
|
||||||
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 'nope' })
|
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 'nope' })
|
||||||
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 0 })
|
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 0 })
|
||||||
timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: {
|
timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: {
|
||||||
timer_mode: 'countup',
|
timer_mode: 'countup',
|
||||||
timer_end_seconds: 60
|
timestamp_seconds: 60
|
||||||
})
|
})
|
||||||
timer_forbidden = other_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 60 })
|
timer_forbidden = other_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 60 })
|
||||||
|
|
||||||
destroy_response = owner_client.delete("/tournaments/#{tournament.fetch(:id)}")
|
destroy_response = owner_client.delete("/tournaments/#{tournament.fetch(:id)}")
|
||||||
after_destroy = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}")
|
after_destroy = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}")
|
||||||
|
|
@ -160,26 +160,26 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(unauthenticated_update[:status]).to eq(401)
|
expect(unauthenticated_update[:status]).to eq(401)
|
||||||
|
|
||||||
expect(timer_seconds[:status]).to eq(200)
|
expect(timer_seconds[:status]).to eq(200)
|
||||||
expect(timer_seconds.dig(:json, :timer_end)).to be_a(String)
|
expect(timer_seconds.dig(:json, :timestamp)).to be_a(String)
|
||||||
expect(timer_seconds.dig(:json, :timer_mode)).to eq('countdown')
|
expect(timer_seconds.dig(:json, :timer_mode)).to eq('countdown')
|
||||||
expect(timer_show[:status]).to eq(200)
|
expect(timer_show[:status]).to eq(200)
|
||||||
expect(timer_show.dig(:json, :timer_end)).to eq(timer_seconds.dig(:json, :timer_end))
|
expect(timer_show.dig(:json, :timestamp)).to eq(timer_seconds.dig(:json, :timestamp))
|
||||||
expect(timer_show.dig(:json, :timer_mode)).to eq('countdown')
|
expect(timer_show.dig(:json, :timer_mode)).to eq('countdown')
|
||||||
expect(timer_countup[:status]).to eq(200)
|
expect(timer_countup[:status]).to eq(200)
|
||||||
expect(timer_countup.dig(:json, :timer_end)).to be_a(String)
|
expect(timer_countup.dig(:json, :timestamp)).to be_a(String)
|
||||||
expect(timer_countup.dig(:json, :timer_mode)).to eq('countup')
|
expect(timer_countup.dig(:json, :timer_mode)).to eq('countup')
|
||||||
expect(timer_clear[:status]).to eq(200)
|
expect(timer_clear[:status]).to eq(200)
|
||||||
expect(timer_clear.dig(:json, :timer_end)).to be_nil
|
expect(timer_clear.dig(:json, :timestamp)).to be_nil
|
||||||
expect(timer_clear.dig(:json, :timer_mode)).to be_nil
|
expect(timer_clear.dig(:json, :timer_mode)).to be_nil
|
||||||
|
|
||||||
expect(timer_double[:status]).to eq(422)
|
expect(timer_double[:status]).to eq(422)
|
||||||
expect(timer_double.dig(:json, :error)).to eq('Only one of timer_end or timer_end_seconds is allowed')
|
expect(timer_double.dig(:json, :error)).to eq('Only one of timestamp or timestamp_seconds is allowed')
|
||||||
expect(timer_invalid_seconds[:status]).to eq(422)
|
expect(timer_invalid_seconds[:status]).to eq(422)
|
||||||
expect(timer_invalid_seconds.dig(:json, :error)).to eq('Invalid seconds format')
|
expect(timer_invalid_seconds.dig(:json, :error)).to eq('Invalid seconds format')
|
||||||
expect(timer_past[:status]).to eq(422)
|
expect(timer_past[:status]).to eq(422)
|
||||||
expect(timer_past.dig(:json, :error)).to eq('Timer end must be in the future')
|
expect(timer_past.dig(:json, :error)).to eq('Timestamp must be in the future for countdown mode')
|
||||||
expect(timer_countup_seconds[:status]).to eq(422)
|
expect(timer_countup_seconds[:status]).to eq(422)
|
||||||
expect(timer_countup_seconds.dig(:json, :error)).to eq('timer_end_seconds is only allowed for countdown mode')
|
expect(timer_countup_seconds.dig(:json, :error)).to eq('timestamp_seconds is only allowed for countdown mode')
|
||||||
expect(timer_forbidden[:status]).to eq(403)
|
expect(timer_forbidden[:status]).to eq(403)
|
||||||
|
|
||||||
expect([200, 204]).to include(destroy_response[:status])
|
expect([200, 204]).to include(destroy_response[:status])
|
||||||
|
|
|
||||||
|
|
@ -474,7 +474,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
playoff_teams_amount: tournament.fetch(:playoff_teams_amount),
|
playoff_teams_amount: tournament.fetch(:playoff_teams_amount),
|
||||||
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
||||||
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
||||||
timer_end: tournament[:timer_end],
|
timestamp: tournament[:timestamp],
|
||||||
teams: tournament.fetch(:teams).map { |team| team.slice(:name, :advancing_from_group_stage) }.sort_by { |team| team.fetch(:name) },
|
teams: tournament.fetch(:teams).map { |team| team.slice(:name, :advancing_from_group_stage) }.sort_by { |team| team.fetch(:name) },
|
||||||
team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list|
|
team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,26 +38,26 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
||||||
cable_client.subscribe_timer!(tournament_id: follower.fetch(:id))
|
cable_client.subscribe_timer!(tournament_id: follower.fetch(:id))
|
||||||
initial_payload = cable_client.wait_for_timer_payload!
|
initial_payload = cable_client.wait_for_timer_payload!
|
||||||
expect(initial_payload.fetch('timer_end')).to be_nil
|
expect(initial_payload.fetch('timestamp')).to be_nil
|
||||||
expect(initial_payload.fetch('timer_mode')).to be_nil
|
expect(initial_payload.fetch('timer_mode')).to be_nil
|
||||||
|
|
||||||
countdown = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 120 })
|
countdown = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 120 })
|
||||||
expect(countdown[:status]).to eq(200)
|
expect(countdown[:status]).to eq(200)
|
||||||
|
|
||||||
countdown_payload = wait_for_timer_state!(cable_client, timer_mode: 'countdown')
|
countdown_payload = wait_for_timer_state!(cable_client, timer_mode: 'countdown')
|
||||||
expect(countdown_payload.fetch('timer_end')).to eq(countdown.dig(:json, :timer_end))
|
expect(countdown_payload.fetch('timestamp')).to eq(countdown.dig(:json, :timestamp))
|
||||||
|
|
||||||
countup = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
countup = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
||||||
expect(countup[:status]).to eq(200)
|
expect(countup[:status]).to eq(200)
|
||||||
|
|
||||||
countup_payload = wait_for_timer_state!(cable_client, timer_mode: 'countup')
|
countup_payload = wait_for_timer_state!(cable_client, timer_mode: 'countup')
|
||||||
expect(countup_payload.fetch('timer_end')).to eq(countup.dig(:json, :timer_end))
|
expect(countup_payload.fetch('timestamp')).to eq(countup.dig(:json, :timestamp))
|
||||||
|
|
||||||
clear = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
clear = source_client.patch("/tournaments/#{source.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
||||||
expect(clear[:status]).to eq(200)
|
expect(clear[:status]).to eq(200)
|
||||||
|
|
||||||
cleared_payload = wait_for_timer_state!(cable_client, timer_mode: nil)
|
cleared_payload = wait_for_timer_state!(cable_client, timer_mode: nil)
|
||||||
expect(cleared_payload.fetch('timer_end')).to be_nil
|
expect(cleared_payload.fetch('timestamp')).to be_nil
|
||||||
ensure
|
ensure
|
||||||
cable_client&.close
|
cable_client&.close
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,26 +29,26 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
||||||
cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id))
|
cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id))
|
||||||
|
|
||||||
initial_payload = cable_client.wait_for_timer_payload!
|
initial_payload = cable_client.wait_for_timer_payload!
|
||||||
expect(initial_payload.fetch('timer_end')).to be_nil
|
expect(initial_payload.fetch('timestamp')).to be_nil
|
||||||
expect(initial_payload.fetch('timer_mode')).to be_nil
|
expect(initial_payload.fetch('timer_mode')).to be_nil
|
||||||
|
|
||||||
countdown = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 120 })
|
countdown = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timestamp_seconds: 120 })
|
||||||
expect(countdown[:status]).to eq(200)
|
expect(countdown[:status]).to eq(200)
|
||||||
|
|
||||||
countdown_payload = wait_for_timer_state!(cable_client, timer_mode: 'countdown')
|
countdown_payload = wait_for_timer_state!(cable_client, timer_mode: 'countdown')
|
||||||
expect(countdown_payload.fetch('timer_end')).to eq(countdown.dig(:json, :timer_end))
|
expect(countdown_payload.fetch('timestamp')).to eq(countdown.dig(:json, :timestamp))
|
||||||
|
|
||||||
countup = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
countup = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_mode: 'countup' })
|
||||||
expect(countup[:status]).to eq(200)
|
expect(countup[:status]).to eq(200)
|
||||||
|
|
||||||
countup_payload = wait_for_timer_state!(cable_client, timer_mode: 'countup')
|
countup_payload = wait_for_timer_state!(cable_client, timer_mode: 'countup')
|
||||||
expect(countup_payload.fetch('timer_end')).to eq(countup.dig(:json, :timer_end))
|
expect(countup_payload.fetch('timestamp')).to eq(countup.dig(:json, :timestamp))
|
||||||
|
|
||||||
clear = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
clear = client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { clear_timer: true })
|
||||||
expect(clear[:status]).to eq(200)
|
expect(clear[:status]).to eq(200)
|
||||||
|
|
||||||
cleared_payload = wait_for_timer_state!(cable_client, timer_mode: nil)
|
cleared_payload = wait_for_timer_state!(cable_client, timer_mode: nil)
|
||||||
expect(cleared_payload.fetch('timer_end')).to be_nil
|
expect(cleared_payload.fetch('timestamp')).to be_nil
|
||||||
ensure
|
ensure
|
||||||
cable_client&.close
|
cable_client&.close
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
playoff_teams_amount: tournament.playoff_teams_amount,
|
playoff_teams_amount: tournament.playoff_teams_amount,
|
||||||
instant_finalists_amount: tournament.instant_finalists_amount,
|
instant_finalists_amount: tournament.instant_finalists_amount,
|
||||||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||||
timer_end: tournament.timer_end&.iso8601,
|
timestamp: tournament.timer_end&.iso8601,
|
||||||
timer_mode: tournament.timer_mode,
|
timer_mode: tournament.timer_mode,
|
||||||
teams: tournament.teams.order(:name).map { |team| team.name },
|
teams: tournament.teams.order(:name).map { |team| team.name },
|
||||||
team_action_lists: tournament.team_action_lists.order(:key).map do |list|
|
team_action_lists: tournament.team_action_lists.order(:key).map do |list|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue