bingo: serve datacenter bingo on bingo.asxp.io
Some checks failed
Tests / test (push) Successful in 18s
Build and Deploy to Production / test (push) Successful in 18s
Build and Deploy to Production / build (push) Failing after 2s
Build and Deploy to Production / deploy (push) Has been skipped

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
This commit is contained in:
Sergei Poljanski 2026-10-10 04:51:26 +04:00
commit bdb3682e18
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
19 changed files with 1428 additions and 2 deletions

145
test/bingo_test.rb Normal file
View file

@ -0,0 +1,145 @@
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