Add a second public site to the same process: a 5x5 card of the 25 largest Russian datacenters where a cell is crossed out once an incident with a public source is recorded (Yandex Sasovo and Kaluga drone strikes on 2026-10-08/09 are seeded). config.ru dispatches by Host, so it rides the existing container, DB and bucket instead of a new deploy. The card is localized ru/en/uk with per-locale columns falling back to ru. Content is managed from /admin/bingo on asxp.io, reusing AdminAuth and CSRF. Photos go to the existing bucket under bingo/ and are proxied by the app, so the bucket stays private and storage needs no change. The seed data was fact-checked against operator sites and press on 2026-10-10; several entries in the original list were wrong or vague. Assisted-by: Claude:opus-5
145 lines
5.4 KiB
Ruby
145 lines
5.4 KiB
Ruby
require_relative 'test_helper'
|
||
|
||
class BingoTest < Minitest::Test
|
||
include Rack::Test::Methods
|
||
|
||
JPEG = "\xFF\xD8\xFF\xE0fakejpeg".b
|
||
|
||
def app
|
||
@app || BingoWeb
|
||
end
|
||
|
||
def setup
|
||
skip 'TEST_DATABASE_URL not set' unless TestDb.available?
|
||
DB.connection[:bingo_incidents].delete
|
||
DB.connection[:bingo_datacenters].delete
|
||
@dcs = (1..25).map do |n|
|
||
Bingo::Datacenter.create(slug: "dc-#{n}", position: n, name_ru: "ЦОД #{n}", name_en: ("DC #{n}" if n == 1),
|
||
city_ru: 'Москва', city_en: 'Moscow', operator: 'Op', mw: n.to_s)
|
||
end
|
||
end
|
||
|
||
def admin_env
|
||
{ 'HTTP_AUTHORIZATION' =>
|
||
"Basic #{Base64.strict_encode64("#{ENV['ADMIN_USER']}:#{ENV['ADMIN_PASSWORD']}")}" }
|
||
end
|
||
|
||
def hit!(idx, kind: 'outage')
|
||
Bingo::Incident.create(datacenter_id: @dcs[idx].id, occurred_on: Date.today, kind: kind,
|
||
title_ru: "Авария #{idx}", title_uk: "Аварія #{idx}")
|
||
end
|
||
|
||
def test_root_redirects_by_accept_language
|
||
get '/', {}, 'HTTP_ACCEPT_LANGUAGE' => 'uk-UA,uk;q=0.9,en;q=0.8'
|
||
assert_equal 302, last_response.status
|
||
assert_match %r{/uk\z}, last_response.location
|
||
get '/', {}, 'HTTP_ACCEPT_LANGUAGE' => 'de-DE'
|
||
assert_match %r{/en\z}, last_response.location
|
||
end
|
||
|
||
def test_locales_render_with_ru_fallback
|
||
get '/en'
|
||
assert last_response.ok?
|
||
assert_includes last_response.body, 'DC 1' # localized
|
||
assert_includes last_response.body, 'ЦОД 2' # falls back to ru
|
||
assert_includes last_response.body, 'Moscow'
|
||
get '/uk'
|
||
assert_includes last_response.body, 'Бінго'
|
||
get '/xx'
|
||
assert_equal 404, last_response.status
|
||
end
|
||
|
||
def test_board_detects_lines
|
||
(0..4).each { |i| hit!(i) } # top row
|
||
[6, 12, 18, 24].each { |i| hit!(i) } # + main diagonal (0 already hit)
|
||
board = Bingo::Board.load
|
||
assert_equal 2, board.wins.size
|
||
get '/ru'
|
||
assert_includes last_response.body, 'БИНГО ×2!'
|
||
assert_includes last_response.body, 'Авария 24'
|
||
get '/uk'
|
||
assert_includes last_response.body, 'Аварія 24'
|
||
end
|
||
|
||
def test_no_banner_without_line
|
||
hit!(0)
|
||
get '/ru'
|
||
refute_includes last_response.body, 'class="banner"'
|
||
assert_includes last_response.body, 'Авария 0'
|
||
end
|
||
|
||
def test_photo_store_and_serve
|
||
Aws.config[:s3] = { stub_responses: { get_object: { body: JPEG } } }
|
||
S3.instance_variable_set(:@client, nil)
|
||
key = Bingo::Photo.store('dc-1', JPEG)
|
||
assert_match %r{\Abingo/dc-1-[0-9a-f]{8}\.jpg\z}, key
|
||
@dcs[0].update(photo_key: key)
|
||
get "/p/#{@dcs[0].photo_file}"
|
||
assert last_response.ok?
|
||
assert_equal 'image/jpeg', last_response.content_type
|
||
get '/p/..%2Finvoices%2Fx.pdf'
|
||
assert_equal 404, last_response.status
|
||
ensure
|
||
Aws.config[:s3] = { stub_responses: true }
|
||
S3.instance_variable_set(:@client, nil)
|
||
end
|
||
|
||
def test_photo_rejects_non_images
|
||
assert_raises(Bingo::Photo::Error) { Bingo::Photo.store('dc-1', '<html>') }
|
||
end
|
||
|
||
# --- admin (served by AsxpioWeb) -------------------------------------------
|
||
|
||
def test_admin_requires_auth
|
||
@app = AsxpioWeb
|
||
get '/admin/bingo'
|
||
assert_equal 401, last_response.status
|
||
end
|
||
|
||
def test_admin_records_and_deletes_incident
|
||
@app = AsxpioWeb
|
||
get '/admin/bingo', {}, admin_env
|
||
assert last_response.ok?
|
||
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
||
|
||
post '/admin/bingo/incidents', { authenticity_token: token, datacenter_id: @dcs[3].id,
|
||
occurred_on: '2026-10-01', kind: 'fire', title_ru: 'Пожар',
|
||
url: 'https://example.com/news' }, admin_env
|
||
assert_equal 302, last_response.status
|
||
inc = Bingo::Incident.first
|
||
assert_equal 'fire', inc.kind
|
||
assert_equal @dcs[3].id, inc.datacenter_id
|
||
|
||
post '/admin/bingo/incidents', { authenticity_token: token, datacenter_id: @dcs[3].id,
|
||
occurred_on: '2026-10-01', kind: 'fire', title_ru: 'x',
|
||
url: 'javascript:alert(1)' }, admin_env
|
||
assert_equal 1, Bingo::Incident.count
|
||
|
||
post "/admin/bingo/incidents/#{inc.id}/delete", { authenticity_token: token }, admin_env
|
||
assert_equal 0, Bingo::Incident.count
|
||
end
|
||
|
||
def test_admin_edits_datacenter
|
||
@app = AsxpioWeb
|
||
get "/admin/bingo/dc/#{@dcs[0].id}", {}, admin_env
|
||
assert last_response.ok?
|
||
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
||
post "/admin/bingo/dc/#{@dcs[0].id}", { authenticity_token: token, name_ru: 'Новое', city_ru: 'Калуга',
|
||
name_uk: 'Нове', operator: 'Yandex', mw: '63', status: 'building',
|
||
year: '2027', position: '1' }, admin_env
|
||
@dcs[0].refresh
|
||
assert_equal 'Нове', @dcs[0].name_uk
|
||
assert @dcs[0].building?
|
||
assert_equal 2027, @dcs[0].year
|
||
end
|
||
|
||
def test_upload_photo_via_admin
|
||
@app = AsxpioWeb
|
||
get "/admin/bingo/dc/#{@dcs[0].id}", {}, admin_env
|
||
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
||
file = Rack::Test::UploadedFile.new(StringIO.new(JPEG), 'image/jpeg', original_filename: 'a.jpg')
|
||
post "/admin/bingo/dc/#{@dcs[0].id}/photo", { authenticity_token: token, photo: file }, admin_env
|
||
assert_equal 302, last_response.status
|
||
assert_match %r{\Abingo/dc-1-}, @dcs[0].refresh.photo_key
|
||
end
|
||
end
|