The datacenter text said "No. 33 by power" for a site that is 25th on the default card, because it counted construction projects. Rank among operational datacenters only, matching the card, and leave the rank out for sites not yet in service. data.json and status.json gain an operational_rank next to the list position. The status page now lists, under each site, the hosts it probes with their label, last result and the source that ties them to the site, folded away behind a click; the control hosts in Russia get the same list, and datacenter pages show it under their reachability block. status.json carries the same targets. For search: a keyword title tagline and meta description, WebPage and Dataset structured data (pointing at status.json), x-default, and the status pages in the sitemap. Assisted-by: Claude:opus-5-5
117 lines
4.6 KiB
Ruby
117 lines
4.6 KiB
Ruby
require_relative 'test_helper'
|
|
|
|
class StatusTest < Minitest::Test
|
|
include Rack::Test::Methods
|
|
|
|
def app = BingoWeb
|
|
|
|
NOW = Time.utc(2026, 10, 18, 12, 2)
|
|
B = Time.utc(2026, 10, 18, 12, 0) # newest bucket at NOW
|
|
|
|
def setup
|
|
skip 'TEST_DATABASE_URL not set' unless TestDb.available?
|
|
@db = DB.connection
|
|
%i[bingo_monitor_buckets bingo_monitor_samples bingo_monitor_targets bingo_incidents bingo_datacenters].each { |t| @db[t].delete }
|
|
@up, @down, @part, @none = %w[up down part none].each_with_index.map do |s, i|
|
|
Bingo::Datacenter.create(slug: "dc-#{s}", position: i + 1, name_ru: "ЦОД #{s}", city_ru: 'Москва',
|
|
operator: 'Op', status: 'operational')
|
|
end
|
|
[@up, @down, @part].each do |dc|
|
|
@db[:bingo_monitor_targets].insert(datacenter_id: dc.id, host: "h-#{dc.slug}", port: 443, label: "probe #{dc.slug}",
|
|
source_url: 'https://example.com/src', last_ok: true, last_rtt_ms: 40, created_at: B)
|
|
end
|
|
@db[:bingo_monitor_targets].insert(datacenter_id: nil, host: 'ctl', port: 80, created_at: B)
|
|
end
|
|
|
|
def bucket(at, control_ok: 4, control_checks: 4)
|
|
@db[:bingo_monitor_buckets].insert(bucket_at: at, rounds: 5, control_checks: control_checks, control_ok: control_ok)
|
|
end
|
|
|
|
def sample(dc, at, ok, checks = 5)
|
|
@db[:bingo_monitor_samples].insert(datacenter_id: dc.id, bucket_at: at, checks: checks, ok: ok, rtt_ms_sum: ok * 40)
|
|
end
|
|
|
|
def two_buckets(base = B, **controls)
|
|
bucket(base - 3600, **controls) # the dark site answered an hour earlier
|
|
sample(@down, base - 3600, 5)
|
|
[base - 300, base].each do |at|
|
|
bucket(at, **controls)
|
|
sample(@up, at, 5)
|
|
sample(@down, at, 0)
|
|
sample(@part, at, 2)
|
|
end
|
|
end
|
|
|
|
def states(report) = report.sites.to_h { |s| [s.dc.slug, s.state] }
|
|
|
|
def test_states_from_newest_buckets
|
|
two_buckets
|
|
r = Bingo::Status.load(NOW)
|
|
assert_equal({ 'dc-up' => 'up', 'dc-down' => 'down', 'dc-part' => 'degraded' }, states(r))
|
|
assert_equal ['dc-none'], r.unmonitored.map(&:slug)
|
|
assert_equal 40, r.sites.first.rtt_ms
|
|
assert_equal 'ok', r.state
|
|
end
|
|
|
|
def test_target_that_never_answered_is_unknown_not_down
|
|
[B - 300, B].each { |at| bucket(at); sample(@down, at, 0) }
|
|
assert_equal 'unknown', states(Bingo::Status.load(NOW))['dc-down']
|
|
end
|
|
|
|
def test_one_dark_bucket_after_a_good_one_is_not_yet_down
|
|
bucket(B - 300); sample(@down, B - 300, 5)
|
|
bucket(B); sample(@down, B, 0)
|
|
assert_equal 'degraded', states(Bingo::Status.load(NOW))['dc-down']
|
|
end
|
|
|
|
def test_lost_vantage_makes_everything_unknown
|
|
two_buckets(control_ok: 1, control_checks: 4)
|
|
r = Bingo::Status.load(NOW)
|
|
assert_equal %w[unknown], states(r).values.uniq
|
|
assert_equal 'vantage', r.state
|
|
assert_nil r.sites.first.history.last, 'untrusted hour shows no data'
|
|
end
|
|
|
|
def test_stale_worker_makes_everything_unknown
|
|
two_buckets
|
|
r = Bingo::Status.load(NOW + 3600)
|
|
assert_equal %w[unknown], states(r).values.uniq
|
|
assert_equal 'stale', r.state
|
|
end
|
|
|
|
def test_status_page_and_json
|
|
two_buckets(Time.at(Time.now.to_i / 300 * 300).utc) # pages use the real clock
|
|
get '/ru/status'
|
|
assert last_response.ok?
|
|
assert_includes last_response.body, 'не отвечает'
|
|
assert_includes last_response.body, 'ЦОД none' # unmonitored list
|
|
get '/status.json'
|
|
data = JSON.parse(last_response.body)
|
|
assert_equal %w[dc-up dc-down dc-part], data['datacenters'].map { |d| d['slug'] }
|
|
assert_equal ['dc-none'], data['unmonitored']
|
|
assert_equal({ 'host' => 'h-dc-up', 'port' => 443, 'label' => 'probe dc-up', 'source_url' => 'https://example.com/src',
|
|
'answered' => true, 'rtt_ms' => 40 }, data['datacenters'][0]['targets'][0])
|
|
assert_equal ['ctl'], data['control_hosts'].map { |t| t['host'] }
|
|
get '/ru/status'
|
|
assert_includes last_response.body, 'h-dc-part:443'
|
|
assert_includes last_response.body, '"@type":"Dataset"'
|
|
get '/en/dc/dc-up'
|
|
assert_includes last_response.body, 'Network reachability'
|
|
get '/en/dc/dc-none'
|
|
refute_includes last_response.body, 'Network reachability'
|
|
end
|
|
|
|
def test_rank_counts_operational_sites_only
|
|
@part.update(status: 'building') # position 3 is not in service
|
|
get '/en/dc/dc-none' # position 4
|
|
assert_includes last_response.body, 'No. 3 by power among'
|
|
get '/en/dc/dc-part'
|
|
refute_includes last_response.body, 'by power among'
|
|
end
|
|
|
|
def test_status_page_before_any_round
|
|
get '/en/status'
|
|
assert last_response.ok?
|
|
assert_includes last_response.body, 'No checks yet.'
|
|
end
|
|
end
|