Initial import of the public datacenter bingo site
The bingo card ran inside the asxpio container, dispatched by Host. Give the public site its own repo, image and deploy so it can be released and mirrored independently of asxp.io. Editing stays in the asxp.io admin, which keeps owning the schema and the photo pipeline; this app only reads the shared bingo tables and the bingo/ photos, through a read-only DB role and MinIO user. Tests load a snapshot of the asxpio-built schema (test/schema.sql). Assisted-by: Claude:opus-5
This commit is contained in:
commit
062611fa21
35 changed files with 2050 additions and 0 deletions
356
test/bingo_test.rb
Normal file
356
test/bingo_test.rb
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
require_relative 'test_helper'
|
||||
|
||||
class BingoTest < Minitest::Test
|
||||
include Rack::Test::Methods
|
||||
|
||||
# Minimal well-formed JPEG: SOI, start-of-scan, data, EOI.
|
||||
JPEG = ("\xFF\xD8\xFF\xDA".b + [8].pack('n') + "\1\2\3\4\5\6data\xFF\xD9".b).freeze
|
||||
|
||||
def app = BingoWeb
|
||||
|
||||
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 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, 'Бінго'
|
||||
assert_includes last_response.body, '<a href="https://asxp.io">asxp.io</a>'
|
||||
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_operational_view_skips_construction_projects
|
||||
@dcs[0].update(status: 'building')
|
||||
Bingo::Datacenter.create(slug: 'dc-26', position: 26, name_ru: 'ЦОД 26', city_ru: 'Москва', operator: 'Op', mw: '1')
|
||||
all = Bingo::Board.load
|
||||
op = Bingo::Board.load('operational')
|
||||
assert_includes all.cells.map(&:slug), 'dc-1'
|
||||
refute_includes op.cells.map(&:slug), 'dc-1'
|
||||
assert_equal 'dc-26', op.cells.last.slug # reserve moves up to fill the card
|
||||
assert_equal 25, op.cells.size
|
||||
end
|
||||
|
||||
def test_operational_view_recomputes_lines
|
||||
(1..4).each { |i| hit!(i) } # row 1 minus dc-1
|
||||
@dcs[0].update(status: 'building')
|
||||
Bingo::Datacenter.create(slug: 'dc-26', position: 26, name_ru: 'ЦОД 26', city_ru: 'Москва', operator: 'Op')
|
||||
assert_equal 0, Bingo::Board.load.wins.size # dc-1 not hit
|
||||
hit!(5) # dc-6 now sits at index 4 in operational view
|
||||
assert_equal 1, Bingo::Board.load('operational').wins.size
|
||||
end
|
||||
|
||||
def test_default_view_is_operational_and_all_is_a_noindex_variant
|
||||
@dcs[0].update(status: 'building')
|
||||
get '/ru'
|
||||
assert last_response.ok?
|
||||
refute_includes last_response.body, 'ЦОД 1<' # construction project hidden by default
|
||||
assert_includes last_response.body, 'index, follow, max-image-preview'
|
||||
assert_includes last_response.body, '<a href="/ru" aria-current="page">'
|
||||
get '/ru?view=all'
|
||||
assert last_response.ok?
|
||||
assert_includes last_response.body, 'ЦОД 1<'
|
||||
assert_includes last_response.body, 'noindex, follow'
|
||||
assert_includes last_response.body, 'href="/en?view=all"' # language links keep the view
|
||||
assert_includes last_response.body, '<a href="/ru?view=all" aria-current="page">'
|
||||
assert_includes last_response.body, 'rel="canonical" href="https://example.org/ru"'
|
||||
get '/ru?view=operational' # old links still land on the default
|
||||
assert_includes last_response.body, 'index, follow, max-image-preview'
|
||||
end
|
||||
|
||||
def test_capacity_type_and_planned_status
|
||||
@dcs[0].update(mw: '64', mw_type: 'design', status: 'planned')
|
||||
@dcs[1].update(mw: '17', mw_type: 'mva')
|
||||
get '/en?view=all' # planned sites only appear in the full list
|
||||
body = last_response.body
|
||||
assert_includes body, '64 MW · design' # dialog
|
||||
assert_includes body, '#1 · 64 MW</div>' # cell: no tag
|
||||
assert_includes body, '17 MVA'
|
||||
assert_includes body, '#2 · 17 MVA</div>' # MVA replaces the unit in cells too
|
||||
assert_includes body, '>planned<'
|
||||
refute_includes Bingo::Board.load('operational').cells.map(&:slug), 'dc-1'
|
||||
end
|
||||
|
||||
def test_provider_filter_keeps_a_5x5_card
|
||||
@dcs.first(3).each { |d| d.update(provider: 'yandex') }
|
||||
@dcs[3].update(provider: 'rtk')
|
||||
hit!(0)
|
||||
board = Bingo::Board.load('operational', providers: %w[yandex rtk])
|
||||
assert_equal %w[dc-1 dc-2 dc-3 dc-4], board.cells.map(&:slug)
|
||||
get '/ru?p[]=yandex&p[]=rtk&p[]=bogus'
|
||||
assert last_response.ok?
|
||||
body = last_response.body
|
||||
assert_equal 4, body.scan(/class="cell(?! empty)[ "]/).size # the four selected datacenters
|
||||
assert_equal 21, body.scan('class="cell empty"').size
|
||||
assert_includes body, 'noindex, follow'
|
||||
assert_includes body, 'rel="canonical" href="https://example.org/ru"'
|
||||
assert_includes body, 'href="/en?p[]=yandex&p[]=rtk"' # language links keep the selection
|
||||
assert_includes body, 'name="p[]" value="yandex" checked'
|
||||
refute_includes body, 'ЦОД 5<' # unselected provider
|
||||
get '/ru?p=yandex,rtk' # comma form works too
|
||||
assert_equal 21, last_response.body.scan('class="cell empty"').size
|
||||
end
|
||||
|
||||
def test_filtered_card_still_scores_lines
|
||||
@dcs.first(5).each { |d| d.update(provider: 'ixcellerate') }
|
||||
(0..4).each { |i| hit!(i) }
|
||||
assert_equal 1, Bingo::Board.load('operational', providers: ['ixcellerate']).wins.size
|
||||
end
|
||||
|
||||
def test_footer_credits_in_every_language
|
||||
{ 'ru' => 'с помощью Claude', 'en' => 'with the help of Claude', 'uk' => 'за допомогою Claude' }.each do |l, text|
|
||||
get "/#{l}"
|
||||
assert_includes last_response.body, text
|
||||
assert_includes last_response.body, '<a href="https://hackyourmom.com/en/karta-data-czentriv-na-rasii/" target="_blank" rel="noopener">hackyourmom</a>'
|
||||
end
|
||||
get "/en/dc/#{@dcs[0].slug}"
|
||||
assert_includes last_response.body, 'hackyourmom</a>'
|
||||
end
|
||||
|
||||
def test_robots_and_sitemap_are_the_bingos_own
|
||||
get '/robots.txt'
|
||||
assert last_response.ok?
|
||||
assert_includes last_response.body, 'Sitemap: https://example.org/sitemap.xml'
|
||||
refute_includes last_response.body, 'asxp.io'
|
||||
get '/sitemap.xml'
|
||||
assert last_response.ok?
|
||||
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}</loc>" }
|
||||
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/dc/dc-25</loc>" }
|
||||
assert_includes last_response.body, '<xhtml:link rel="alternate" hreflang="uk" href="https://example.org/uk/dc/dc-7"/>'
|
||||
assert_equal 3, last_response.body.scan('hreflang="x-default"').size # card URLs only
|
||||
assert_includes last_response.body, 'hreflang="x-default"'
|
||||
refute_includes last_response.body, 'changefreq'
|
||||
assert_match %r{</loc>\n <lastmod>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ</lastmod>\n <xhtml:link}, last_response.body
|
||||
end
|
||||
|
||||
def test_trailing_slash_redirects
|
||||
get '/en/', view: 'all'
|
||||
assert_equal 301, last_response.status
|
||||
assert_equal 'http://example.org/en?view=all', last_response.location
|
||||
end
|
||||
|
||||
def test_icons
|
||||
{ '/favicon.svg' => 'image/svg+xml', '/favicon.ico' => 'image/x-icon',
|
||||
'/apple-touch-icon.png' => 'image/png' }.each do |path, type|
|
||||
get path
|
||||
assert last_response.ok?, path
|
||||
assert_equal type, last_response.content_type
|
||||
end
|
||||
get '/en'
|
||||
assert_includes last_response.body, '<link rel="icon" href="/favicon.svg" type="image/svg+xml">'
|
||||
end
|
||||
|
||||
def test_hsts
|
||||
get '/en'
|
||||
assert_equal 'max-age=31536000', last_response.headers['strict-transport-security']
|
||||
assert_equal 'nosniff', last_response.headers['x-content-type-options']
|
||||
end
|
||||
|
||||
def test_dc_page
|
||||
hit!(3)
|
||||
get '/en/dc/dc-4'
|
||||
assert last_response.ok?
|
||||
body = last_response.body
|
||||
assert_includes body, "<title>ЦОД 4 (Moscow) — Outage, #{Date.today.strftime('%d.%m.%Y')} | Datacenter Bingo</title>"
|
||||
assert_includes body, '<link rel="canonical" href="https://example.org/en/dc/dc-4">'
|
||||
assert_includes body, '<link rel="alternate" hreflang="uk" href="https://example.org/uk/dc/dc-4">'
|
||||
refute_includes body, 'x-default'
|
||||
assert_includes body, '<h1>ЦОД 4</h1>'
|
||||
assert_includes body, 'ЦОД 4 is a datacenter in Moscow operated by Op. Capacity: 4 MW.'
|
||||
assert_includes body, 'No. 4 by power'
|
||||
assert_includes body, 'href="/en/dc/dc-3" rel="prev"'
|
||||
assert_includes body, 'href="/en/dc/dc-5" rel="next"'
|
||||
assert_match(/<meta name="description" content="[^"]*Latest reported incident/, body)
|
||||
ld = JSON.parse(body[%r{<script type="application/ld\+json">(.*?)</script>}m, 1])
|
||||
place = ld['@graph'].find { |n| n['@type'] == 'Place' }
|
||||
assert_equal 'https://example.org/en/dc/dc-4', place['url']
|
||||
assert_equal 'Outage: Авария 3', place['event'][0]['name']
|
||||
crumbs = ld['@graph'].find { |n| n['@type'] == 'BreadcrumbList' }
|
||||
assert_equal 'https://example.org/en', crumbs['itemListElement'][0]['item']
|
||||
|
||||
@dcs[3].update(mw_type: 'design')
|
||||
get '/en/dc/dc-4'
|
||||
assert_includes last_response.body, 'Capacity: 4 MW (design).'
|
||||
get '/uk/dc/dc-1'
|
||||
assert_includes last_response.body, '<title>ЦОД 1 (Москва) — інциденти ЦОДу | Бінго ЦОДів</title>'
|
||||
refute_includes last_response.body, 'rel="prev"'
|
||||
end
|
||||
|
||||
def test_reserve_dc_has_a_page
|
||||
Bingo::Datacenter.create(slug: 'dc-26', position: 26, name_ru: 'ЦОД 26', city_ru: 'Тверь', operator: 'Op')
|
||||
get '/ru/dc/dc-26'
|
||||
assert last_response.ok?
|
||||
assert_includes last_response.body, 'href="/ru/dc/dc-25" rel="prev"'
|
||||
get '/sitemap.xml'
|
||||
assert_includes last_response.body, '<loc>https://example.org/ru/dc/dc-26</loc>'
|
||||
end
|
||||
|
||||
def test_dc_page_unknown_and_trailing_slash
|
||||
get '/en/dc/nope'
|
||||
assert_equal 404, last_response.status
|
||||
get '/en/dc/dc-1/'
|
||||
assert_equal 301, last_response.status
|
||||
assert_equal 'http://example.org/en/dc/dc-1', last_response.location
|
||||
end
|
||||
|
||||
def test_card_links_to_dc_pages
|
||||
hit!(0)
|
||||
get '/en'
|
||||
body = last_response.body
|
||||
assert_includes body, '<a href="/en/dc/dc-1" data-dlg="dlg-dc-1"'
|
||||
assert_includes body, '<a href="/en/dc/dc-2">Datacenter page →</a>'
|
||||
assert_includes body, '<div class="dc"><a href="/en/dc/dc-1">DC 1</a>'
|
||||
end
|
||||
|
||||
def with_env(vars)
|
||||
old = vars.to_h { |k, _| [k, ENV[k]] }
|
||||
vars.each { |k, v| ENV[k] = v }
|
||||
yield
|
||||
ensure
|
||||
old.each { |k, v| ENV[k] = v }
|
||||
end
|
||||
|
||||
def test_indexnow_key_file
|
||||
get '/0123456789abcdef.txt'
|
||||
assert_equal 404, last_response.status
|
||||
with_env('INDEXNOW_KEY' => '0123456789abcdef') do
|
||||
get '/0123456789abcdef.txt'
|
||||
assert last_response.ok?
|
||||
assert_equal '0123456789abcdef', last_response.body
|
||||
get '/fedcba9876543210.txt'
|
||||
assert_equal 404, last_response.status
|
||||
end
|
||||
end
|
||||
|
||||
def test_root_varies_on_language
|
||||
get '/'
|
||||
assert_equal 'Accept-Language', last_response.headers['Vary']
|
||||
end
|
||||
|
||||
def test_seo_head
|
||||
hit!(3)
|
||||
@dcs[3].update(photo_key: 'bingo/dc-4-0123abcd.jpg', photo_variants: true)
|
||||
@dcs[0].update(info_url: 'https://example.com/dc-1')
|
||||
get '/en'
|
||||
body = last_response.body
|
||||
assert_includes body, '<title>Datacenter Bingo — strikes'
|
||||
assert_match %r{<h1>Datacenter <span>Bingo</span> <small>Drone strikes, fires and outages at Russia's 25 largest datacenters</small></h1>}, body
|
||||
assert_includes body, '1 of Russia's 25 largest'
|
||||
assert_includes body, '<meta property="og:image" content="https://example.org/p/dc-4-0123abcd-1200.webp">'
|
||||
assert_includes body, 'src="/p/dc-4-0123abcd-480.webp"'
|
||||
refute_includes body, 'og:image:width'
|
||||
@dcs[3].update(photo_og: true)
|
||||
get '/en'
|
||||
body = last_response.body
|
||||
assert_includes body, '<meta property="og:image" content="https://example.org/p/dc-4-0123abcd-og.jpg">'
|
||||
assert_includes body, '<meta property="og:image:width" content="1200">'
|
||||
assert_includes body, '<meta property="og:image:alt" content="ЦОД 4, Moscow">'
|
||||
assert_includes body, 'og:locale:alternate" content="uk_UA"'
|
||||
ld = JSON.parse(body[%r{<script type="application/ld\+json">(.*?)</script>}m, 1])
|
||||
list = ld['@graph'].find { |n| n['@type'] == 'ItemList' }
|
||||
assert_equal 25, list['itemListElement'].size
|
||||
assert_equal 'DC 1', list['itemListElement'][0]['item']['name']
|
||||
hit = list['itemListElement'][3]['item']
|
||||
assert_equal Date.today.iso8601, hit['event'][0]['startDate']
|
||||
assert_equal 'Outage: Авария 3', hit['event'][0]['name']
|
||||
refute list['itemListElement'][0]['item'].key?('event')
|
||||
assert_equal({ '@type' => 'CreativeWork', 'url' => 'https://example.com/dc-1' }, list['itemListElement'][0]['item']['subjectOf'])
|
||||
refute_includes body, 'sameAs'
|
||||
assert list['dateModified']
|
||||
end
|
||||
|
||||
def test_photo_falls_back_to_original_without_variants
|
||||
@dcs[0].update(photo_key: 'bingo/dc-1-0123abcd.jpg', photo_variants: false)
|
||||
assert_equal 'dc-1-0123abcd.jpg', @dcs[0].photo_file(480)
|
||||
@dcs[0].update(photo_variants: true)
|
||||
assert_equal 'dc-1-0123abcd-480.webp', @dcs[0].photo_file(480)
|
||||
assert_equal 'dc-1-0123abcd.jpg', @dcs[0].photo_file
|
||||
end
|
||||
|
||||
def test_photo_served_from_bucket
|
||||
Aws.config[:s3] = { stub_responses: { get_object: { body: JPEG } } }
|
||||
S3.instance_variable_set(:@client, nil)
|
||||
@dcs[0].update(photo_key: 'bingo/dc-1-0123abcd.jpg')
|
||||
get "/p/#{@dcs[0].photo_file}"
|
||||
assert last_response.ok?
|
||||
assert_equal 'image/jpeg', last_response.content_type
|
||||
assert_includes last_response.headers['cache-control'], 'immutable'
|
||||
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_dc_about_text
|
||||
@dcs[1].update(about_en: "First <b>para</b>.\r\n\r\nSecond.")
|
||||
get '/en/dc/dc-2'
|
||||
assert_includes last_response.body, '<p class="facts">First <b>para</b>.</p><p class="facts">Second.</p>'
|
||||
get '/ru/dc/dc-2'
|
||||
refute_includes last_response.body, 'Second.' # no fallback across languages
|
||||
end
|
||||
|
||||
def test_static_files_not_served
|
||||
get '/style.css'
|
||||
assert_equal 404, last_response.status
|
||||
end
|
||||
|
||||
def test_healthz
|
||||
get '/healthz'
|
||||
assert last_response.ok?
|
||||
assert_equal 'ok', last_response.body
|
||||
end
|
||||
|
||||
def test_www_redirects_to_canonical_host
|
||||
ENV['BINGO_HOST'] = 'bingo.example'
|
||||
app = Rack::Builder.parse_file(File.join(__dir__, '..', 'config.ru'))
|
||||
res = Rack::MockRequest.new(app).get('http://www.bingo.example/en?view=all')
|
||||
assert_equal 301, res.status
|
||||
assert_equal 'https://bingo.example/en?view=all', res.location
|
||||
ensure
|
||||
ENV.delete('BINGO_HOST')
|
||||
end
|
||||
end
|
||||
78
test/schema.sql
Normal file
78
test/schema.sql
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
-- bingo tables as created by the asxpio migrations (004–016), for tests.
|
||||
-- Refresh when asxpio adds a bingo migration: run them into an empty DB and
|
||||
-- pg_dump -s -O -x -t bingo_datacenters -t bingo_incidents.
|
||||
CREATE TABLE public.bingo_datacenters (
|
||||
id integer NOT NULL,
|
||||
slug text NOT NULL,
|
||||
"position" integer NOT NULL,
|
||||
name_ru text NOT NULL,
|
||||
name_en text,
|
||||
name_uk text,
|
||||
city_ru text NOT NULL,
|
||||
city_en text,
|
||||
city_uk text,
|
||||
operator text NOT NULL,
|
||||
mw text,
|
||||
status text DEFAULT 'operational'::text NOT NULL,
|
||||
year integer,
|
||||
photo_key text,
|
||||
photo_source_url text,
|
||||
photo_credit text,
|
||||
info_url text,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
photo_variants boolean DEFAULT false NOT NULL,
|
||||
mw_type text,
|
||||
photo_og boolean DEFAULT false NOT NULL,
|
||||
updated_at timestamp without time zone,
|
||||
about_ru text,
|
||||
about_en text,
|
||||
about_uk text,
|
||||
provider text
|
||||
);
|
||||
|
||||
ALTER TABLE public.bingo_datacenters ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.bingo_datacenters_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.bingo_incidents (
|
||||
id integer NOT NULL,
|
||||
datacenter_id integer NOT NULL,
|
||||
occurred_on date NOT NULL,
|
||||
kind text NOT NULL,
|
||||
title_ru text NOT NULL,
|
||||
title_en text,
|
||||
title_uk text,
|
||||
url text,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone
|
||||
);
|
||||
|
||||
ALTER TABLE public.bingo_incidents ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.bingo_incidents_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.bingo_datacenters
|
||||
ADD CONSTRAINT bingo_datacenters_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.bingo_datacenters
|
||||
ADD CONSTRAINT bingo_datacenters_slug_key UNIQUE (slug);
|
||||
|
||||
ALTER TABLE ONLY public.bingo_incidents
|
||||
ADD CONSTRAINT bingo_incidents_pkey PRIMARY KEY (id);
|
||||
|
||||
CREATE INDEX bingo_incidents_datacenter_id_index ON public.bingo_incidents USING btree (datacenter_id);
|
||||
|
||||
ALTER TABLE ONLY public.bingo_incidents
|
||||
ADD CONSTRAINT bingo_incidents_datacenter_id_fkey FOREIGN KEY (datacenter_id) REFERENCES public.bingo_datacenters(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
13
test/templates_test.rb
Normal file
13
test/templates_test.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
require_relative 'test_helper'
|
||||
|
||||
# A template with unbalanced <% end %> only fails when that page is
|
||||
# rendered; compile every view up front so CI catches it regardless.
|
||||
class TemplatesTest < Minitest::Test
|
||||
Dir[File.join(__dir__, '..', 'views', '**', '*.erb')].each do |path|
|
||||
name = path.sub(%r{.*/views/}, '').tr('/.', '__')
|
||||
define_method("test_compiles_#{name}") do
|
||||
tmpl = Tilt::ErubiTemplate.new(path, escape_html: true)
|
||||
tmpl.send(:compiled_method, [], Object) # raises SyntaxError if broken
|
||||
end
|
||||
end
|
||||
end
|
||||
29
test/test_helper.rb
Normal file
29
test/test_helper.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
ENV['RACK_ENV'] = 'test'
|
||||
|
||||
# DB-backed tests run only against an explicit test database (bin/test
|
||||
# provisions an ephemeral one). Never fall through to a developer DATABASE_URL.
|
||||
if ENV['TEST_DATABASE_URL']
|
||||
ENV['DATABASE_URL'] = ENV['TEST_DATABASE_URL']
|
||||
ENV['S3_ENDPOINT'] ||= 'http://127.0.0.1:1'
|
||||
ENV['S3_ACCESS_KEY'] ||= 'test'
|
||||
ENV['S3_SECRET_KEY'] ||= 'test'
|
||||
ENV['S3_BUCKET'] ||= 'test-bucket'
|
||||
require 'aws-sdk-s3'
|
||||
Aws.config[:s3] = { stub_responses: true } # no S3 call leaves the process
|
||||
|
||||
# The schema is asxpio's; load the snapshot into the empty test database.
|
||||
require 'sequel'
|
||||
db = Sequel.connect(ENV['DATABASE_URL'])
|
||||
db.run(File.read(File.join(__dir__, 'schema.sql'))) unless db.table_exists?(:bingo_datacenters)
|
||||
db.disconnect
|
||||
else
|
||||
ENV.delete('DATABASE_URL')
|
||||
end
|
||||
|
||||
require_relative '../app'
|
||||
require 'minitest/autorun'
|
||||
require 'rack/test'
|
||||
|
||||
module TestDb
|
||||
def self.available? = !ENV['DATABASE_URL'].nil?
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue