bingo: serve datacenter bingo on bingo.asxp.io
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:
parent
1f059dc2f6
commit
bdb3682e18
19 changed files with 1428 additions and 2 deletions
145
test/bingo_test.rb
Normal file
145
test/bingo_test.rb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue