Merge branch 'backend-e2e-api-coverage' into 'master'
Expand-backend-HTTP-E2E-API-coverage See merge request turniere/turniere-backend!33
This commit is contained in:
commit
ec18e7aff3
|
|
@ -67,6 +67,10 @@ module TurniereE2E
|
|||
request(:patch, path, body: body, params: params, headers: headers)
|
||||
end
|
||||
|
||||
def delete(path, params: nil, headers: {})
|
||||
request(:delete, path, params: params, headers: headers)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(method, path, body: nil, params: nil, headers: {})
|
||||
|
|
@ -77,6 +81,7 @@ module TurniereE2E
|
|||
when :get then Net::HTTP::Get
|
||||
when :post then Net::HTTP::Post
|
||||
when :patch then Net::HTTP::Patch
|
||||
when :delete then Net::HTTP::Delete
|
||||
else
|
||||
raise ArgumentError, "unsupported method #{method}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,409 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'securerandom'
|
||||
require_relative '../../../e2e/lib/api_client'
|
||||
|
||||
RSpec.describe 'Backend API surface HTTP E2E' do
|
||||
primary_env_vars = %w[TURNIERE_E2E_EMAIL TURNIERE_E2E_PASSWORD].freeze
|
||||
|
||||
before do
|
||||
missing_vars = primary_env_vars.reject { |name| ENV.key?(name) }
|
||||
skip("HTTP E2E requires #{missing_vars.join(', ')}") unless missing_vars.empty?
|
||||
end
|
||||
|
||||
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') }
|
||||
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
||||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||
let(:owner_username) { ENV.fetch('TURNIERE_E2E_USERNAME', owner_email) }
|
||||
|
||||
let(:owner_client) { login_client(email: owner_email, password: owner_password) }
|
||||
let(:other_client) { secondary_client }
|
||||
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) }
|
||||
|
||||
it 'reports health' do
|
||||
response = anonymous_client.get('/healthz')
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
expect(response.dig(:json, :errors)).to eq([])
|
||||
end
|
||||
|
||||
it 'supports registration for new users and still requires confirmation for login' do
|
||||
unique = unique_suffix
|
||||
email = "http-e2e-#{unique}@example.com"
|
||||
password = 'password123'
|
||||
username = "http-e2e-#{unique}"
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
registration = client.post('/users', body: {
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
username: username
|
||||
})
|
||||
login = TurniereE2E::ApiClient.new(base_url: base_url).login!(email: email, password: password)
|
||||
|
||||
expect([500, *200..299]).to include(registration[:status])
|
||||
expect(registration[:json]).not_to eq({})
|
||||
expect(login[:status]).to eq(401)
|
||||
end
|
||||
|
||||
it 'lists public and private tournaments and supports simple and full show responses' do
|
||||
public_tournament = create_playoff_tournament(client: owner_client, public: true, name_prefix: 'Public Listing')
|
||||
private_tournament = create_playoff_tournament(client: owner_client, public: false, name_prefix: 'Private Listing')
|
||||
|
||||
public_index = anonymous_client.get('/tournaments')
|
||||
private_index = owner_client.get('/tournaments', params: { type: 'private' })
|
||||
simple_show = anonymous_client.get("/tournaments/#{public_tournament.fetch(:id)}", params: { simple: 'true' })
|
||||
full_show = anonymous_client.get("/tournaments/#{public_tournament.fetch(:id)}")
|
||||
invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' })
|
||||
|
||||
expect(public_index[:status]).to eq(200)
|
||||
expect(public_index[:json].map { |tournament| tournament[:id] }).to include(public_tournament.fetch(:id))
|
||||
expect(public_index[:json].map { |tournament| tournament[:id] }).not_to include(private_tournament.fetch(:id))
|
||||
|
||||
expect(private_index[:status]).to eq(200)
|
||||
expect(private_index[:json].map { |tournament| tournament[:id] }).to include(private_tournament.fetch(:id))
|
||||
expect(private_index[:json].map { |tournament| tournament[:id] }).not_to include(public_tournament.fetch(:id))
|
||||
|
||||
expect(simple_show[:status]).to eq(200)
|
||||
expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id))
|
||||
expect(simple_show[:json]).not_to have_key(:stages)
|
||||
|
||||
expect(full_show[:status]).to eq(200)
|
||||
expect(full_show.dig(:json, :stages)).to be_an(Array)
|
||||
expect(full_show.dig(:json, :teams)).to be_an(Array)
|
||||
|
||||
expect(invalid_index[:status]).to eq(400)
|
||||
expect(invalid_index.dig(:json, :error)).to eq('invalid type')
|
||||
end
|
||||
|
||||
it 'creates group-stage and playoff-only tournaments and rejects invalid creation payloads' do
|
||||
group_stage = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Create Group')
|
||||
playoff_only = create_playoff_tournament(client: owner_client, public: false, name_prefix: 'Create Playoff')
|
||||
|
||||
invalid_teams = owner_client.post('/tournaments', body: {
|
||||
name: "Invalid Teams #{unique_suffix}",
|
||||
public: false,
|
||||
teams: ['nope']
|
||||
})
|
||||
unauthenticated = anonymous_client.post('/tournaments', body: {
|
||||
name: "Needs Auth #{unique_suffix}",
|
||||
public: false,
|
||||
teams: [{ name: 'Solo Team' }]
|
||||
})
|
||||
|
||||
expect(group_stage.fetch(:stages).map { |stage| stage[:level] }).to include(-1)
|
||||
expect(group_stage.fetch(:teams).count).to eq(16)
|
||||
|
||||
expect(playoff_only.fetch(:stages).map { |stage| stage[:level] }.sort).to eq([0, 1])
|
||||
expect(playoff_only.fetch(:teams).count).to eq(4)
|
||||
|
||||
expect(invalid_teams[:status]).to eq(422)
|
||||
expect(invalid_teams.dig(:json, :error)).to eq('Invalid teams array')
|
||||
|
||||
expect(unauthenticated[:status]).to eq(401)
|
||||
end
|
||||
|
||||
it 'updates and destroys tournaments, enforces owner checks, and validates timer updates' do
|
||||
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Mutable Tournament')
|
||||
|
||||
update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: {
|
||||
name: "Renamed #{unique_suffix}",
|
||||
playoff_teams_amount: 8,
|
||||
instant_finalists_amount: 8,
|
||||
intermediate_round_participants_amount: 0
|
||||
})
|
||||
auto_recalc_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: {
|
||||
playoff_teams_amount: 4
|
||||
})
|
||||
invalid_update = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: {
|
||||
playoff_teams_amount: 8,
|
||||
instant_finalists_amount: 7,
|
||||
intermediate_round_participants_amount: 0
|
||||
})
|
||||
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' })
|
||||
|
||||
timer_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 120 })
|
||||
timer_show = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/timer_end")
|
||||
timer_double = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: {
|
||||
timer_end_seconds: 120,
|
||||
timer_end: (Time.now.utc + 240).iso8601
|
||||
})
|
||||
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 'nope' })
|
||||
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 0 })
|
||||
timer_forbidden = other_client.patch("/tournaments/#{tournament.fetch(:id)}/set_timer_end", body: { timer_end_seconds: 60 })
|
||||
|
||||
destroy_response = owner_client.delete("/tournaments/#{tournament.fetch(:id)}")
|
||||
after_destroy = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}")
|
||||
|
||||
expect(update_response[:status]).to eq(200)
|
||||
expect(update_response.dig(:json, :name)).to start_with('Renamed')
|
||||
|
||||
expect(auto_recalc_response[:status]).to eq(200)
|
||||
expect(auto_recalc_response.dig(:json, :playoff_teams_amount)).to eq(4)
|
||||
expect(auto_recalc_response.dig(:json, :instant_finalists_amount)).to eq(4)
|
||||
expect(auto_recalc_response.dig(:json, :intermediate_round_participants_amount)).to eq(0)
|
||||
|
||||
expect(invalid_update[:status]).to eq(422)
|
||||
expect(invalid_update.dig(:json, :error)).to include("don't match")
|
||||
|
||||
expect(forbidden_update[:status]).to eq(403)
|
||||
expect(forbidden_update.dig(:json, :errors)).to include('Only the parent tournament owner can update this resource')
|
||||
expect(unauthenticated_update[:status]).to eq(401)
|
||||
|
||||
expect(timer_seconds[:status]).to eq(200)
|
||||
expect(timer_seconds.dig(:json, :timer_end)).to be_a(String)
|
||||
expect(timer_show[:status]).to eq(200)
|
||||
expect(timer_show.dig(:json, :timer_end)).to eq(timer_seconds.dig(:json, :timer_end))
|
||||
|
||||
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_invalid_seconds[:status]).to eq(422)
|
||||
expect(timer_invalid_seconds.dig(:json, :error)).to eq('Invalid seconds format')
|
||||
expect(timer_past[:status]).to eq(422)
|
||||
expect(timer_past.dig(:json, :error)).to eq('Timer end must be in the future')
|
||||
expect(timer_forbidden[:status]).to eq(403)
|
||||
|
||||
expect([200, 204]).to include(destroy_response[:status])
|
||||
expect(after_destroy[:status]).to eq(404)
|
||||
end
|
||||
|
||||
it 'supports match, score, team, group, statistics, stage, and bet flows over HTTP' do
|
||||
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Surface Flow')
|
||||
group_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
group = group_stage.fetch(:groups).min_by { |item| item.fetch(:number) }
|
||||
match = group.fetch(:matches).min_by { |item| item.fetch(:position) }
|
||||
team = match.fetch(:match_scores).first.fetch(:team)
|
||||
|
||||
group_show = anonymous_client.get("/groups/#{group.fetch(:id)}")
|
||||
team_show = anonymous_client.get("/teams/#{team.fetch(:id)}")
|
||||
team_update = owner_client.patch("/teams/#{team.fetch(:id)}", body: { name: "#{team.fetch(:name)} Renamed" })
|
||||
invalid_team_update = owner_client.patch("/teams/#{team.fetch(:id)}", body: { name: '' })
|
||||
forbidden_team_update = other_client.patch("/teams/#{team.fetch(:id)}", body: { name: 'Other User Name' })
|
||||
|
||||
statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
||||
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' })
|
||||
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
||||
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||
invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' })
|
||||
forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||
|
||||
playoff_only = create_playoff_tournament(client: owner_client, public: false, name_prefix: 'Playoff Statistics')
|
||||
unrelated_team = playoff_only.fetch(:teams).first
|
||||
|
||||
bet_create = other_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
|
||||
bet_duplicate = other_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
|
||||
bet_invalid_team = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: unrelated_team.fetch(:id) })
|
||||
bets_index = anonymous_client.get("/matches/#{match.fetch(:id)}/bets")
|
||||
|
||||
start_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
started_match = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||
score_to_update = started_match.fetch(:json).fetch(:match_scores).first
|
||||
score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}")
|
||||
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 })
|
||||
forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
|
||||
unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
|
||||
bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
|
||||
restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics")
|
||||
tied_playoff_match = playoff_only.fetch(:stages)
|
||||
.flat_map { |stage| stage.fetch(:matches) }
|
||||
.find { |playoff_match| playoff_match.fetch(:state) == 'not_started' }
|
||||
owner_client.patch("/matches/#{tied_playoff_match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
tied_match_after_start = anonymous_client.get("/matches/#{tied_playoff_match.fetch(:id)}")
|
||||
tied_match_after_start.fetch(:json).fetch(:match_scores).each do |score|
|
||||
owner_client.patch("/match_scores/#{score.fetch(:id)}", body: { points: 5 })
|
||||
end
|
||||
tied_finish = owner_client.patch("/matches/#{tied_playoff_match.fetch(:id)}", body: { state: 'finished' })
|
||||
|
||||
finish_group_match_with_winner!(match_id: match.fetch(:id))
|
||||
refreshed_tournament = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}")[:json]
|
||||
refreshed_group_stage = refreshed_tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
refreshed_group_stage.fetch(:groups).each do |stage_group|
|
||||
stage_group.fetch(:matches).each do |stage_match|
|
||||
next if stage_match.fetch(:state) == 'finished'
|
||||
|
||||
finish_group_match_with_winner!(match_id: stage_match.fetch(:id))
|
||||
end
|
||||
end
|
||||
|
||||
finished_stage = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||
stage_show = anonymous_client.get("/stages/#{group_stage.fetch(:id)}")
|
||||
statistics_after = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||
|
||||
expect(group_show[:status]).to eq(200)
|
||||
expect(group_show.dig(:json, :matches)).to be_an(Array)
|
||||
|
||||
expect(team_show[:status]).to eq(200)
|
||||
expect(team_update[:status]).to eq(200)
|
||||
expect(team_update.dig(:json, :name)).to end_with('Renamed')
|
||||
expect(invalid_team_update[:status]).to eq(422)
|
||||
expect(forbidden_team_update[:status]).to eq(403)
|
||||
|
||||
expect(statistics_before[:status]).to eq(200)
|
||||
expect(statistics_before.dig(:json, :group_scores)).to be_an(Array)
|
||||
expect(matches_all[:status]).to eq(200)
|
||||
expect(matches_all[:json].count).to eq(tournament_match_count(tournament))
|
||||
expect(matches_not_started[:status]).to eq(200)
|
||||
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||
expect(matches_upcoming[:status]).to eq(200)
|
||||
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||
expect(match_show[:status]).to eq(200)
|
||||
expect(invalid_match_update[:status]).to eq(422)
|
||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||
expect(forbidden_match_update[:status]).to eq(403)
|
||||
expect(unauthenticated_match_update[:status]).to eq(403)
|
||||
|
||||
expect(stage_not_over[:status]).to eq(422)
|
||||
expect(stage_not_over.dig(:json, :error)).to include('Group Stage still has some matches')
|
||||
expect(invalid_stage_update[:status]).to eq(422)
|
||||
expect(invalid_stage_update.dig(:json, :error)).to eq('The state attribute may only be changed to finished')
|
||||
expect(forbidden_stage_update[:status]).to eq(403)
|
||||
|
||||
expect(bet_create[:status]).to eq(200)
|
||||
expect(bet_create.dig(:json, :team, :id)).to eq(team.fetch(:id))
|
||||
expect(bet_duplicate[:status]).to eq(422)
|
||||
expect(bet_duplicate.dig(:json, :error)).to eq('This user already created a bet on this match')
|
||||
expect(bet_invalid_team[:status]).to eq(422)
|
||||
expect(bet_invalid_team.dig(:json, :error)).to eq('The given team is not involved in the given match')
|
||||
expect(bets_index[:status]).to eq(200)
|
||||
expect(bets_index[:json].count).to eq(1)
|
||||
|
||||
expect(start_match[:status]).to eq(200)
|
||||
expect(score_show[:status]).to eq(200)
|
||||
expect(score_update[:status]).to eq(200)
|
||||
expect(score_update.dig(:json, :points)).to eq(7)
|
||||
expect(forbidden_score_update[:status]).to eq(403)
|
||||
expect(unauthenticated_score_update[:status]).to eq(401)
|
||||
expect(bet_while_running[:status]).to eq(422)
|
||||
expect(bet_while_running.dig(:json, :error)).to eq('Betting is not allowed while match is in_progress')
|
||||
expect(restart_started_match[:status]).to eq(422)
|
||||
expect(restart_started_match.dig(:json, :error)).to eq("Match can't start in this state")
|
||||
|
||||
expect(playoff_stats[:status]).to eq(501)
|
||||
expect(playoff_stats[:json]).to eq({})
|
||||
expect(tied_finish[:status]).to eq(422)
|
||||
expect(tied_finish.dig(:json, :error)).to eq("Stopping undecided Matches isn't allowed in playoff stage")
|
||||
|
||||
expect(finished_stage[:status]).to eq(200)
|
||||
expect(stage_show[:status]).to eq(200)
|
||||
expect(stage_show.dig(:json, :state)).to eq('finished')
|
||||
expect(statistics_after[:status]).to eq(200)
|
||||
expect(statistics_after.dig(:json, :group_scores).count).to eq(16)
|
||||
expect(statistics_after.dig(:json, :most_dominant_score, :id)).to be_a(Integer)
|
||||
expect(statistics_after.dig(:json, :least_dominant_score, :id)).to be_a(Integer)
|
||||
end
|
||||
|
||||
def login_client(email:, password:)
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: email, password: password)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
expect(client.authenticated?).to eq(true)
|
||||
client
|
||||
end
|
||||
|
||||
def secondary_client
|
||||
alt_email = ENV['TURNIERE_E2E_ALT_EMAIL']
|
||||
alt_password = ENV['TURNIERE_E2E_ALT_PASSWORD']
|
||||
if alt_email && alt_password
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: alt_email, password: alt_password)
|
||||
return client if response[:status] == 200 && client.authenticated?
|
||||
end
|
||||
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
unique = unique_suffix
|
||||
response = client.post('/users', body: {
|
||||
email: "http-e2e-secondary-#{unique}@example.com",
|
||||
password: 'password123',
|
||||
password_confirmation: 'password123',
|
||||
username: "http-e2e-secondary-#{unique}"
|
||||
})
|
||||
|
||||
expect(response[:status]).to be_between(200, 299)
|
||||
expect(client.authenticated?).to eq(true)
|
||||
client
|
||||
end
|
||||
|
||||
def create_group_stage_tournament(client:, public:, name_prefix:)
|
||||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'HTTP API surface E2E group-stage tournament',
|
||||
public: public,
|
||||
group_stage: true,
|
||||
playoff_teams_amount: 8,
|
||||
teams: 4.times.flat_map do |group_index|
|
||||
4.times.map do |seed_index|
|
||||
{
|
||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||
group: group_index
|
||||
}
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
expect(response[:status]).to eq(201)
|
||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
||||
end
|
||||
|
||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
||||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'HTTP API surface E2E playoff tournament',
|
||||
public: public,
|
||||
teams: 4.times.map do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||
end
|
||||
}
|
||||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
expect(response[:status]).to eq(201)
|
||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
||||
end
|
||||
|
||||
def fetch_tournament(client:, tournament_id:)
|
||||
response = client.get("/tournaments/#{tournament_id}")
|
||||
expect(response[:status]).to eq(200)
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
||||
def finish_group_match_with_winner!(match_id:)
|
||||
match = anonymous_client.get("/matches/#{match_id}")
|
||||
expect(match[:status]).to eq(200)
|
||||
|
||||
unless match.dig(:json, :state) == 'in_progress'
|
||||
start_response = owner_client.patch("/matches/#{match_id}", body: { state: 'in_progress' })
|
||||
expect(start_response[:status]).to eq(200)
|
||||
match = anonymous_client.get("/matches/#{match_id}")
|
||||
end
|
||||
|
||||
ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
||||
ordered_scores.each_with_index do |score, index|
|
||||
response = owner_client.patch("/match_scores/#{score.fetch(:id)}", body: { points: index.zero? ? 10 : 1 })
|
||||
expect(response[:status]).to eq(200)
|
||||
end
|
||||
|
||||
finish_response = owner_client.patch("/matches/#{match_id}", body: { state: 'finished' })
|
||||
expect(finish_response[:status]).to eq(200)
|
||||
end
|
||||
|
||||
def tournament_match_count(tournament)
|
||||
tournament.fetch(:stages).sum do |stage|
|
||||
stage.fetch(:matches, []).count + stage.fetch(:groups, []).sum { |group| group.fetch(:matches).count }
|
||||
end
|
||||
end
|
||||
|
||||
def unique_suffix
|
||||
@unique_suffix_counter ||= 0
|
||||
@unique_suffix_counter += 1
|
||||
"#{SecureRandom.hex(3)}-#{@unique_suffix_counter}"
|
||||
end
|
||||
end
|
||||
61
tasks.py
61
tasks.py
|
|
@ -8,6 +8,7 @@ import subprocess
|
|||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
|
@ -20,6 +21,9 @@ BLACKBOX_INTERNAL_BASE_URL = "http://app:3000"
|
|||
E2E_EMAIL = "e2e@example.com"
|
||||
E2E_PASSWORD = "password123"
|
||||
E2E_USERNAME = "e2e-user"
|
||||
E2E_ALT_EMAIL = "e2e-alt@example.com"
|
||||
E2E_ALT_PASSWORD = "password123"
|
||||
E2E_ALT_USERNAME = "e2e-alt-user"
|
||||
|
||||
PRODUCTION_TAG = "registry.gitlab.com/turniere/turniere-backend/production/commits:local"
|
||||
DEVELOPMENT_TAG = "registry.gitlab.com/turniere/turniere-backend/development/commits:local"
|
||||
|
|
@ -123,6 +127,9 @@ def _compose_env(
|
|||
TURNIERE_E2E_EMAIL=E2E_EMAIL,
|
||||
TURNIERE_E2E_PASSWORD=E2E_PASSWORD,
|
||||
TURNIERE_E2E_USERNAME=E2E_USERNAME,
|
||||
TURNIERE_E2E_ALT_EMAIL=E2E_ALT_EMAIL,
|
||||
TURNIERE_E2E_ALT_PASSWORD=E2E_ALT_PASSWORD,
|
||||
TURNIERE_E2E_ALT_USERNAME=E2E_ALT_USERNAME,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -237,12 +244,16 @@ def _shared_production_env():
|
|||
)
|
||||
|
||||
|
||||
def _run_blackbox_rspec(base_url, email, password, username):
|
||||
def _run_blackbox_rspec(base_url, email, password, username, alt_email=E2E_ALT_EMAIL, alt_password=E2E_ALT_PASSWORD,
|
||||
alt_username=E2E_ALT_USERNAME):
|
||||
env = _env(
|
||||
TURNIERE_E2E_BASE_URL=base_url,
|
||||
TURNIERE_E2E_EMAIL=email,
|
||||
TURNIERE_E2E_PASSWORD=password,
|
||||
TURNIERE_E2E_USERNAME=username,
|
||||
TURNIERE_E2E_ALT_EMAIL=alt_email,
|
||||
TURNIERE_E2E_ALT_PASSWORD=alt_password,
|
||||
TURNIERE_E2E_ALT_USERNAME=alt_username,
|
||||
)
|
||||
command = ["bundle", "exec", "rspec", "spec/e2e/http", "--format", "documentation"]
|
||||
print(_format_command(command))
|
||||
|
|
@ -277,9 +288,9 @@ def _start_server(command, env, log_path="/tmp/turniere-e2e-server.log"):
|
|||
return process, log_handle
|
||||
|
||||
|
||||
def _start_test_server():
|
||||
def _start_test_server(host=SERVER_HOST, port=SERVER_PORT):
|
||||
return _start_server(
|
||||
f"{_bundle_exec('rails', 's')} -e test -b {SERVER_HOST} -p {SERVER_PORT}",
|
||||
f"{_bundle_exec('rails', 's')} -e test -b {host} -p {port}",
|
||||
_env(),
|
||||
)
|
||||
|
||||
|
|
@ -425,6 +436,9 @@ def test_http_e2e(
|
|||
email=E2E_EMAIL,
|
||||
password=E2E_PASSWORD,
|
||||
username=E2E_USERNAME,
|
||||
alt_email=E2E_ALT_EMAIL,
|
||||
alt_password=E2E_ALT_PASSWORD,
|
||||
alt_username=E2E_ALT_USERNAME,
|
||||
):
|
||||
"""Run HTTP E2E specs against an already running backend."""
|
||||
c.run(
|
||||
|
|
@ -434,6 +448,9 @@ def test_http_e2e(
|
|||
TURNIERE_E2E_EMAIL=email,
|
||||
TURNIERE_E2E_PASSWORD=password,
|
||||
TURNIERE_E2E_USERNAME=username,
|
||||
TURNIERE_E2E_ALT_EMAIL=alt_email,
|
||||
TURNIERE_E2E_ALT_PASSWORD=alt_password,
|
||||
TURNIERE_E2E_ALT_USERNAME=alt_username,
|
||||
),
|
||||
pty=True,
|
||||
)
|
||||
|
|
@ -446,15 +463,31 @@ def verify_http(
|
|||
email=E2E_EMAIL,
|
||||
password=E2E_PASSWORD,
|
||||
username=E2E_USERNAME,
|
||||
alt_email=E2E_ALT_EMAIL,
|
||||
alt_password=E2E_ALT_PASSWORD,
|
||||
alt_username=E2E_ALT_USERNAME,
|
||||
):
|
||||
"""Boot a local test server, then run the HTTP E2E suite against it."""
|
||||
db_migrate(c)
|
||||
db_migrate(c, env="test")
|
||||
bootstrap_e2e_user(c, email=email, password=password, username=username)
|
||||
bootstrap_e2e_user(c, email=alt_email, password=alt_password, username=alt_username)
|
||||
parsed_base_url = urlparse(base_url)
|
||||
server_host = parsed_base_url.hostname or "127.0.0.1"
|
||||
server_port = parsed_base_url.port or 3000
|
||||
|
||||
process, log_handle = _start_test_server()
|
||||
process, log_handle = _start_test_server(host=server_host, port=server_port)
|
||||
try:
|
||||
_wait_for_http(base_url=base_url, timeout=60)
|
||||
test_http_e2e(c, base_url=base_url, email=email, password=password, username=username)
|
||||
test_http_e2e(
|
||||
c,
|
||||
base_url=base_url,
|
||||
email=email,
|
||||
password=password,
|
||||
username=username,
|
||||
alt_email=alt_email,
|
||||
alt_password=alt_password,
|
||||
alt_username=alt_username,
|
||||
)
|
||||
finally:
|
||||
_stop_process(process, log_handle)
|
||||
|
||||
|
|
@ -592,8 +625,12 @@ def docker_blackbox_up(
|
|||
_wait_for_http(base_url=base_url, timeout=120)
|
||||
|
||||
_print_header("Bootstrapping confirmed E2E user")
|
||||
for bootstrap_email, bootstrap_password, bootstrap_username in (
|
||||
(E2E_EMAIL, E2E_PASSWORD, E2E_USERNAME),
|
||||
(E2E_ALT_EMAIL, E2E_ALT_PASSWORD, E2E_ALT_USERNAME),
|
||||
):
|
||||
bootstrap = _compose_run(
|
||||
["exec", "-T", "app", "bash", "-lc", _bootstrap_user_command(E2E_EMAIL, E2E_PASSWORD, E2E_USERNAME)],
|
||||
["exec", "-T", "app", "bash", "-lc", _bootstrap_user_command(bootstrap_email, bootstrap_password, bootstrap_username)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
|
|
@ -684,6 +721,9 @@ def blackbox_service(
|
|||
email=E2E_EMAIL,
|
||||
password=E2E_PASSWORD,
|
||||
username=E2E_USERNAME,
|
||||
alt_email=E2E_ALT_EMAIL,
|
||||
alt_password=E2E_ALT_PASSWORD,
|
||||
alt_username=E2E_ALT_USERNAME,
|
||||
):
|
||||
"""Run blackbox HTTP E2E against an already running production app plus Postgres sidecars."""
|
||||
_print_header("Running production database migrations")
|
||||
|
|
@ -698,9 +738,14 @@ def blackbox_service(
|
|||
env=_shared_production_env(),
|
||||
pty=True,
|
||||
)
|
||||
c.run(
|
||||
_bootstrap_user_command(alt_email, alt_password, alt_username),
|
||||
env=_shared_production_env(),
|
||||
pty=True,
|
||||
)
|
||||
|
||||
_print_header("Running HTTP E2E against production image")
|
||||
_run_blackbox_rspec(base_url, email, password, username)
|
||||
_run_blackbox_rspec(base_url, email, password, username, alt_email, alt_password, alt_username)
|
||||
|
||||
|
||||
@task(name="blackbox-production")
|
||||
|
|
|
|||
Loading…
Reference in New Issue